@intentius/chant 0.37.2 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.d.ts +21 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-mcp.d.ts +44 -0
- package/dist/cli/commands/check-lexicon-mcp.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-plugin.d.ts +57 -0
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/handlers/emulator.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts +30 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +26 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +20 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts +23 -0
- package/dist/codegen/registry.d.ts.map +1 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -1
- package/dist/composite.d.ts +23 -4
- package/dist/composite.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +11 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +17 -3
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-refs.d.ts +24 -0
- package/dist/graph-refs.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts.map +1 -1
- package/dist/lexicon-config.d.ts +61 -0
- package/dist/lexicon-config.d.ts.map +1 -0
- package/dist/lexicon.d.ts +85 -37
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/deep-diff.d.ts +11 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/digest.d.ts.map +1 -1
- package/dist/lifecycle/identity.d.ts +52 -0
- package/dist/lifecycle/identity.d.ts.map +1 -0
- package/dist/lifecycle/observe.d.ts +5 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +30 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +21 -22
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/managed-fields.d.ts +11 -0
- package/dist/managed-fields.d.ts.map +1 -1
- package/dist/op/emulator-freshness.d.ts +44 -0
- package/dist/op/emulator-freshness.d.ts.map +1 -0
- package/dist/op/emulator-lifecycle.d.ts +36 -0
- package/dist/op/emulator-lifecycle.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +33 -0
- package/dist/ownership.d.ts.map +1 -1
- package/dist/serializer.d.ts +15 -0
- package/dist/serializer.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.test.ts +58 -6
- package/src/build.ts +24 -0
- package/src/cli/commands/build.ts +10 -0
- package/src/cli/commands/check-lexicon-doc-drift.test.ts +73 -0
- package/src/cli/commands/check-lexicon-mcp.test.ts +93 -0
- package/src/cli/commands/check-lexicon-mcp.ts +103 -0
- package/src/cli/commands/check-lexicon-plugin.test.ts +149 -0
- package/src/cli/commands/check-lexicon-plugin.ts +115 -0
- package/src/cli/commands/check-lexicon.ts +176 -26
- package/src/cli/handlers/components.test.ts +17 -0
- package/src/cli/handlers/components.ts +1 -1
- package/src/cli/handlers/emulator.ts +12 -8
- package/src/cli/handlers/graph.test.ts +71 -12
- package/src/cli/handlers/graph.ts +46 -5
- package/src/cli/handlers/lifecycle.test.ts +25 -4
- package/src/cli/handlers/lifecycle.ts +19 -3
- package/src/cli/handlers/run-client.ts +3 -1
- package/src/cli/handlers/search-kind.test.ts +45 -0
- package/src/cli/handlers/search.ts +102 -4
- package/src/cli/main.ts +32 -10
- package/src/cli/mcp/server.test.ts +82 -0
- package/src/cli/mcp/server.ts +40 -5
- package/src/cli/param-flag-scope.test.ts +69 -0
- package/src/cli/plugins.test.ts +33 -1
- package/src/cli/plugins.ts +55 -0
- package/src/codegen/registry.test.ts +56 -0
- package/src/codegen/registry.ts +69 -0
- package/src/components/SPRAWL-VALIDATION.md +5 -5
- package/src/components/sandbox/driver.test.ts +27 -0
- package/src/components/sandbox/driver.ts +12 -0
- package/src/composite.ts +33 -4
- package/src/deep-observation.ts +11 -0
- package/src/discovery/sandbox/driver.test.ts +34 -0
- package/src/discovery/sandbox/driver.ts +19 -0
- package/src/graph-declared.test.ts +86 -0
- package/src/graph-declared.ts +14 -2
- package/src/graph-ir.ts +32 -8
- package/src/graph-refs.test.ts +56 -0
- package/src/graph-refs.ts +37 -1
- package/src/kubectl-context.ts +4 -1
- package/src/lexicon-config.test.ts +111 -0
- package/src/lexicon-config.ts +92 -0
- package/src/lexicon-doc-coverage.test.ts +128 -0
- package/src/lexicon-seams.test.ts +113 -0
- package/src/lexicon.ts +88 -38
- package/src/lifecycle/deep-diff.test.ts +48 -1
- package/src/lifecycle/deep-diff.ts +16 -0
- package/src/lifecycle/digest.test.ts +81 -0
- package/src/lifecycle/digest.ts +34 -3
- package/src/lifecycle/identity.test.ts +39 -0
- package/src/lifecycle/identity.ts +61 -0
- package/src/lifecycle/observe.test.ts +75 -1
- package/src/lifecycle/observe.ts +28 -2
- package/src/lifecycle/replay.test.ts +251 -0
- package/src/lifecycle/replay.ts +67 -19
- package/src/lifecycle/types.ts +26 -0
- package/src/live-endpoint.test.ts +51 -12
- package/src/live-endpoint.ts +32 -33
- package/src/managed-fields.test.ts +50 -0
- package/src/managed-fields.ts +25 -6
- package/src/meta/peer-deps.test.ts +111 -14
- package/src/op/emulator-declaration.test.ts +63 -0
- package/src/op/emulator-freshness.test.ts +135 -0
- package/src/op/emulator-freshness.ts +102 -0
- package/src/op/emulator-lifecycle.ts +49 -0
- package/src/op/index.ts +4 -2
- package/src/ownership.ts +41 -0
- package/src/serializer.ts +16 -0
|
@@ -350,7 +350,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
|
|
|
350
350
|
const endpointResult = applyLiveEndpoint(
|
|
351
351
|
config.environments,
|
|
352
352
|
environment,
|
|
353
|
-
plugins.filter((p) => p.describeResources || p.describeStackStatus)
|
|
353
|
+
plugins.filter((p) => p.describeResources || p.describeStackStatus),
|
|
354
354
|
);
|
|
355
355
|
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
356
356
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { emulatorLifecycle } from "../../op";
|
|
3
|
+
import { emulatorLifecycle, emulatorsOf } from "../../op";
|
|
4
4
|
import { formatError, formatWarning, formatSuccess } from "../format";
|
|
5
5
|
import type { CommandContext } from "../registry";
|
|
6
6
|
|
|
@@ -48,8 +48,13 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
|
|
|
48
48
|
return 1;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// One entry per emulator, not per lexicon (#1345): fly ships two — mudflaps
|
|
52
|
+
// for the Machines API and spritzer for Sprites — and reporting a lexicon
|
|
53
|
+
// would have to pick one of them.
|
|
54
|
+
const targets = plugins
|
|
55
|
+
.filter((p) => !args.lexicon || p.name === args.lexicon)
|
|
56
|
+
.flatMap((p) => emulatorsOf(p.emulator).map((cap) => ({ lexicon: p.name, cap })));
|
|
57
|
+
if (targets.length === 0) {
|
|
53
58
|
if (args.json) {
|
|
54
59
|
console.log(JSON.stringify({ emulators: [] }));
|
|
55
60
|
return 0;
|
|
@@ -63,19 +68,18 @@ export async function runEmulator(ctx: CommandContext): Promise<number> {
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
const reports: EmulatorReport[] = [];
|
|
66
|
-
for (const
|
|
67
|
-
const cap = p.emulator!;
|
|
71
|
+
for (const { lexicon, cap } of targets) {
|
|
68
72
|
const lc = emulatorLifecycle(cap.spec);
|
|
69
73
|
if (action === "up") {
|
|
70
74
|
const { endpoint } = await lc.up();
|
|
71
|
-
reports.push({ lexicon
|
|
75
|
+
reports.push({ lexicon, name: cap.spec.name, endpoint, env: cap.env(endpoint) });
|
|
72
76
|
} else if (action === "down") {
|
|
73
77
|
await lc.down();
|
|
74
|
-
reports.push({ lexicon
|
|
78
|
+
reports.push({ lexicon, name: cap.spec.name, endpoint: "", env: {} });
|
|
75
79
|
} else {
|
|
76
80
|
const running = await isRunning(cap.spec.name);
|
|
77
81
|
const endpoint = running ? lc.endpoint(cap.spec.containerPort) : "";
|
|
78
|
-
reports.push({ lexicon
|
|
82
|
+
reports.push({ lexicon, name: cap.spec.name, endpoint, env: endpoint ? cap.env(endpoint) : {} });
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
|
|
@@ -3,6 +3,18 @@ import type { ParsedArgs } from "../registry";
|
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
4
4
|
import { AttrRef } from "../../attrref";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The aws emulator capability, as the real plugin declares it. `--live`
|
|
8
|
+
* endpoint injection reads the endpoint var off this rather than off a map
|
|
9
|
+
* keyed by lexicon name (#1345), so a mock that omits it gets no injection —
|
|
10
|
+
* which is the same thing that would happen in production.
|
|
11
|
+
*/
|
|
12
|
+
const awsEmulatorStub = {
|
|
13
|
+
spec: { name: "chant-floci", image: "floci/floci:1.5.34", containerPort: 4566, healthPath: "/_localstack/health" },
|
|
14
|
+
env: (endpoint: string) => ({ AWS_ENDPOINT_URL: endpoint, AWS_ACCESS_KEY_ID: "test" }),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
const discoverOpsMock = vi.fn();
|
|
7
19
|
vi.mock("../../op/discover", () => ({
|
|
8
20
|
discoverOps: () => discoverOpsMock(),
|
|
@@ -10,7 +22,9 @@ vi.mock("../../op/discover", () => ({
|
|
|
10
22
|
|
|
11
23
|
const discoverMock = vi.fn();
|
|
12
24
|
vi.mock("../../discovery/index", () => ({
|
|
13
|
-
|
|
25
|
+
// Forwards its arguments so a test can assert what the handler passed —
|
|
26
|
+
// `buildParams` in particular (#1359).
|
|
27
|
+
discover: (...a: unknown[]) => discoverMock(...a),
|
|
14
28
|
}));
|
|
15
29
|
|
|
16
30
|
const lintMock = vi.fn();
|
|
@@ -62,11 +76,13 @@ vi.mock("../../components/discover", () => ({
|
|
|
62
76
|
discoverComponents: (...a: unknown[]) => discoverComponentsMock(...a),
|
|
63
77
|
}));
|
|
64
78
|
const loadChantConfigMock = vi.fn();
|
|
79
|
+
const loadChantConfigUpwardMock = vi.fn();
|
|
65
80
|
vi.mock("../../config", async () => {
|
|
66
81
|
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
67
82
|
return {
|
|
68
83
|
...actual,
|
|
69
84
|
loadChantConfig: (...a: unknown[]) => loadChantConfigMock(...a),
|
|
85
|
+
loadChantConfigUpward: (...a: unknown[]) => loadChantConfigUpwardMock(...a),
|
|
70
86
|
};
|
|
71
87
|
});
|
|
72
88
|
vi.mock("../../build", () => ({
|
|
@@ -124,6 +140,8 @@ describe("runGraph", () => {
|
|
|
124
140
|
resolveLexMock.mockReset();
|
|
125
141
|
loadChantConfigMock.mockReset();
|
|
126
142
|
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
143
|
+
loadChantConfigUpwardMock.mockReset();
|
|
144
|
+
loadChantConfigUpwardMock.mockResolvedValue({ config: {} });
|
|
127
145
|
});
|
|
128
146
|
|
|
129
147
|
describe("Op graph (default)", () => {
|
|
@@ -186,6 +204,47 @@ describe("runGraph", () => {
|
|
|
186
204
|
expect(ir.edges).toContainEqual({ from: "subnet", to: "vpc", kind: "ref", viaAttr: "network" });
|
|
187
205
|
});
|
|
188
206
|
|
|
207
|
+
// #1339 — `chant graph` discovered source with `params.*` empty, so a
|
|
208
|
+
// declaration conditioned on a build parameter always took its default:
|
|
209
|
+
// the graph showed one shape whatever `--param` or a declared `env:`
|
|
210
|
+
// mapping said, while `chant build` on the same source showed another.
|
|
211
|
+
describe("build-time parameters reach discovery (#1359)", () => {
|
|
212
|
+
const withTierParam = (): void => {
|
|
213
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
214
|
+
config: { buildParams: { tier: { type: "string", enum: ["light", "prod"], default: "light" } } },
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
test("--param is resolved and passed to discover", async () => {
|
|
219
|
+
lintClean(); discovered(); withTierParam();
|
|
220
|
+
const exit = await runGraph({
|
|
221
|
+
args: makeArgs({ format: "ir", param: ["tier=prod"] }),
|
|
222
|
+
plugins: [],
|
|
223
|
+
serializers: [],
|
|
224
|
+
});
|
|
225
|
+
expect(exit).toBe(0);
|
|
226
|
+
const [, options] = discoverMock.mock.calls[0] as [string, { buildParams?: Array<{ name: string; value: unknown }> }];
|
|
227
|
+
expect(options.buildParams).toContainEqual(expect.objectContaining({ name: "tier", value: "prod" }));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("a declared default is passed too, so discovery never sees an empty params object", async () => {
|
|
231
|
+
lintClean(); discovered(); withTierParam();
|
|
232
|
+
await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
|
|
233
|
+
const [, options] = discoverMock.mock.calls[0] as [string, { buildParams?: Array<{ name: string; value: unknown }> }];
|
|
234
|
+
expect(options.buildParams).toContainEqual(expect.objectContaining({ name: "tier", value: "light" }));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("an unresolvable parameter stops the graph rather than emitting a default one", async () => {
|
|
238
|
+
lintClean(); discovered();
|
|
239
|
+
loadChantConfigUpwardMock.mockResolvedValue({
|
|
240
|
+
config: { buildParams: { tier: { type: "string", required: true } } },
|
|
241
|
+
});
|
|
242
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
|
|
243
|
+
expect(exit).toBe(1);
|
|
244
|
+
expect(stdoutBuf.join("\n")).toBe("");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
189
248
|
test("lint gate: refuses to emit when source has lint errors", async () => {
|
|
190
249
|
lintMock.mockResolvedValue({ success: false });
|
|
191
250
|
const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
|
|
@@ -427,7 +486,7 @@ describe("runGraph", () => {
|
|
|
427
486
|
test("loads plugins for --live when ctx.plugins is empty", async () => {
|
|
428
487
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
429
488
|
loadPluginsMock.mockResolvedValue([
|
|
430
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
489
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
431
490
|
]);
|
|
432
491
|
observeMock.mockResolvedValue({
|
|
433
492
|
observations: [{ lexicon: "aws", resources: { "web-vpc": { type: "AWS::EC2::VPC", status: "OK" } } }],
|
|
@@ -448,7 +507,7 @@ describe("runGraph", () => {
|
|
|
448
507
|
test("--at graphs the recorded snapshot without reading the estate", async () => {
|
|
449
508
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
450
509
|
loadPluginsMock.mockResolvedValue([
|
|
451
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}),
|
|
510
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}),
|
|
452
511
|
enrichLiveAttrs: () => Promise.reject(new Error("must not be called on a replay")) },
|
|
453
512
|
]);
|
|
454
513
|
replayMock.mockResolvedValue({
|
|
@@ -473,7 +532,7 @@ describe("runGraph", () => {
|
|
|
473
532
|
hasSnapshotMock.mockResolvedValue(true);
|
|
474
533
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
475
534
|
loadPluginsMock.mockResolvedValue([
|
|
476
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
535
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
477
536
|
]);
|
|
478
537
|
observeMock.mockResolvedValue({ observations: [], errors: ["could not connect"], warnings: [] });
|
|
479
538
|
const errs: string[] = [];
|
|
@@ -489,7 +548,7 @@ describe("runGraph", () => {
|
|
|
489
548
|
hasSnapshotMock.mockResolvedValue(false);
|
|
490
549
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
491
550
|
loadPluginsMock.mockResolvedValue([
|
|
492
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
551
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
493
552
|
]);
|
|
494
553
|
observeMock.mockResolvedValue({ observations: [], errors: ["could not connect"], warnings: [] });
|
|
495
554
|
const errs: string[] = [];
|
|
@@ -515,7 +574,7 @@ describe("runGraph", () => {
|
|
|
515
574
|
test("single-stack project (no components): observeResources gets an empty stacks list", async () => {
|
|
516
575
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
517
576
|
loadPluginsMock.mockResolvedValue([
|
|
518
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
577
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
519
578
|
]);
|
|
520
579
|
observeMock.mockResolvedValue({ observations: [], errors: [], warnings: [] });
|
|
521
580
|
const exit = await runGraph({ args: makeArgs({ format: "ir", live: true, env: "prod" }), plugins: [], serializers: [] });
|
|
@@ -533,7 +592,7 @@ describe("runGraph", () => {
|
|
|
533
592
|
test("multi-stack component project: resolves each component's cfn-deploy stack(s) and passes them to observeResources", async () => {
|
|
534
593
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
535
594
|
loadPluginsMock.mockResolvedValue([
|
|
536
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
595
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
537
596
|
]);
|
|
538
597
|
discoverComponentsMock.mockResolvedValue({
|
|
539
598
|
errors: [],
|
|
@@ -580,7 +639,7 @@ describe("runGraph", () => {
|
|
|
580
639
|
test("ChantConfig.stacks: observeResources gets every declared stack, with its region and src", async () => {
|
|
581
640
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
582
641
|
loadPluginsMock.mockResolvedValue([
|
|
583
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
642
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
584
643
|
]);
|
|
585
644
|
loadChantConfigMock.mockResolvedValue({
|
|
586
645
|
config: {
|
|
@@ -609,7 +668,7 @@ describe("runGraph", () => {
|
|
|
609
668
|
test("ChantConfig.stacks: a stack also derived from a component is not observed twice", async () => {
|
|
610
669
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
611
670
|
loadPluginsMock.mockResolvedValue([
|
|
612
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
671
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
613
672
|
]);
|
|
614
673
|
discoverComponentsMock.mockResolvedValue({
|
|
615
674
|
errors: [],
|
|
@@ -636,7 +695,7 @@ describe("runGraph", () => {
|
|
|
636
695
|
test("component discovery errors: falls back to the single-stack path with a warning", async () => {
|
|
637
696
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
638
697
|
loadPluginsMock.mockResolvedValue([
|
|
639
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
698
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
640
699
|
]);
|
|
641
700
|
discoverComponentsMock.mockResolvedValue({ errors: [{ message: "bad component" }], sourceFiles: [], components: new Map() });
|
|
642
701
|
observeMock.mockResolvedValue({ observations: [], errors: [], warnings: [] });
|
|
@@ -667,7 +726,7 @@ describe("runGraph", () => {
|
|
|
667
726
|
});
|
|
668
727
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
669
728
|
loadPluginsMock.mockResolvedValue([
|
|
670
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
729
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
671
730
|
]);
|
|
672
731
|
let seenDuringObserve: string | undefined;
|
|
673
732
|
observeMock.mockImplementation(async () => {
|
|
@@ -688,7 +747,7 @@ describe("runGraph", () => {
|
|
|
688
747
|
});
|
|
689
748
|
resolveLexMock.mockResolvedValue(["aws"]);
|
|
690
749
|
loadPluginsMock.mockResolvedValue([
|
|
691
|
-
{ name: "aws", serializer: {}, describeResources: () => Promise.resolve({}) },
|
|
750
|
+
{ name: "aws", serializer: {}, emulator: awsEmulatorStub, describeResources: () => Promise.resolve({}) },
|
|
692
751
|
]);
|
|
693
752
|
let seenDuringObserve: string | undefined;
|
|
694
753
|
observeMock.mockImplementation(async () => {
|
|
@@ -7,7 +7,7 @@ import { buildDeclaredPerStack } from "../../graph-declared";
|
|
|
7
7
|
import { reconstructEdges, mergeCatalogs, containmentGroups, type ReferenceCatalog, type ContainmentPair } from "../../graph-refs";
|
|
8
8
|
import { observeResources } from "../../lifecycle/observe";
|
|
9
9
|
import { replaySnapshots, hasSnapshot } from "../../lifecycle/replay";
|
|
10
|
-
import { loadChantConfig, environmentNames } from "../../config";
|
|
10
|
+
import { loadChantConfig, environmentNames, loadChantConfigUpward, type ChantConfig } from "../../config";
|
|
11
11
|
import { applyLiveEndpoint } from "../../live-endpoint";
|
|
12
12
|
import { applyDetail, type DetailLevel } from "../../graph-detail";
|
|
13
13
|
import { applyLens, parseLens } from "../../graph-lens";
|
|
@@ -20,9 +20,42 @@ import { readFileSync } from "node:fs";
|
|
|
20
20
|
import { formatError, formatWarning, formatBold } from "../format";
|
|
21
21
|
import type { CommandContext } from "../registry";
|
|
22
22
|
import { computeComponentGraph, generateComponentsPipeline } from "../../components/cli-support";
|
|
23
|
+
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
24
|
+
import type { BuildParamProvenance } from "../../provenance";
|
|
23
25
|
import { discoverComponents } from "../../components/discover";
|
|
24
26
|
import { cfnDeployStacks } from "./components";
|
|
25
27
|
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve this invocation's declared build-time parameters, the same way
|
|
31
|
+
* `chant build` does, so `discover()` sees the values the source will read.
|
|
32
|
+
*
|
|
33
|
+
* Without this `chant graph` discovered source with `params.*` empty, so every
|
|
34
|
+
* declaration conditioned on a parameter took its default — a project whose
|
|
35
|
+
* tier is a `buildParams` entry graphed as one tier whatever `--param tier=`
|
|
36
|
+
* or the declared `env:` mapping said, while `chant build` on the same source
|
|
37
|
+
* and the same environment emitted a different resource set. Two commands
|
|
38
|
+
* disagreeing about what the source says.
|
|
39
|
+
*
|
|
40
|
+
* Returns `undefined` when resolution failed, having printed why — the caller
|
|
41
|
+
* should stop rather than graph something that will not build.
|
|
42
|
+
*/
|
|
43
|
+
async function graphBuildParams(
|
|
44
|
+
ctx: CommandContext,
|
|
45
|
+
projectPath: string,
|
|
46
|
+
): Promise<BuildParamProvenance[] | undefined> {
|
|
47
|
+
const { config } = await loadChantConfigUpward(projectPath).catch(() => ({ config: {} as ChantConfig }));
|
|
48
|
+
const resolution = resolveCliBuildParams(config.buildParams, {
|
|
49
|
+
cli: parseParamFlags(ctx.args.param),
|
|
50
|
+
paramsFile: ctx.args.paramsFile,
|
|
51
|
+
});
|
|
52
|
+
if (!resolution.success) {
|
|
53
|
+
for (const message of resolution.errors) console.error(message);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return resolution.provenance;
|
|
57
|
+
}
|
|
58
|
+
|
|
26
59
|
/**
|
|
27
60
|
* `chant graph` — the Op dependency graph by default; `--stacks` renders the
|
|
28
61
|
* cross-stack apply-ordering graph (edges, order, waves) chant computes from
|
|
@@ -158,7 +191,7 @@ async function runGraphLive(
|
|
|
158
191
|
// exported e.g. AWS_ENDPOINT_URL. Ambient always wins when it's already set.
|
|
159
192
|
// Scoped to just this describe/enrich pass — restored in `finally` so it
|
|
160
193
|
// never leaks into a later invocation in the same process.
|
|
161
|
-
const endpointResult = applyLiveEndpoint(config.environments, environment, observing
|
|
194
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, observing);
|
|
162
195
|
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
163
196
|
|
|
164
197
|
let ir: GraphIR;
|
|
@@ -271,7 +304,10 @@ async function runGraphLive(
|
|
|
271
304
|
? overlayGraphs(ir, declaredIr, overlayOpts)
|
|
272
305
|
: sourceOverlayGraphs(declaredIr, ir, overlayOpts);
|
|
273
306
|
} else {
|
|
274
|
-
const
|
|
307
|
+
const declaredParams = await graphBuildParams(ctx, projectPath);
|
|
308
|
+
const declared = await discover(resolve(args.src ?? config.sourceDir ?? "."), {
|
|
309
|
+
...(declaredParams ? { buildParams: declaredParams } : {}),
|
|
310
|
+
});
|
|
275
311
|
if (declared.errors.length === 0) {
|
|
276
312
|
const declaredIr = buildGraphIr(declared.entities, projectPath);
|
|
277
313
|
ir =
|
|
@@ -487,7 +523,10 @@ async function runGraphView(
|
|
|
487
523
|
return 1;
|
|
488
524
|
}
|
|
489
525
|
|
|
490
|
-
const
|
|
526
|
+
const buildParams = await graphBuildParams(ctx, projectPath);
|
|
527
|
+
if (!buildParams) return 1;
|
|
528
|
+
|
|
529
|
+
const result = await discover(projectPath, { buildParams });
|
|
491
530
|
if (result.errors.length > 0) {
|
|
492
531
|
for (const e of result.errors) console.error(formatError({ message: e.message }));
|
|
493
532
|
return 1;
|
|
@@ -596,7 +635,9 @@ async function runOpGraph(): Promise<number> {
|
|
|
596
635
|
|
|
597
636
|
async function runStackGraph(ctx: CommandContext): Promise<number> {
|
|
598
637
|
const projectPath = resolve(ctx.args.path === "." ? "." : ctx.args.path);
|
|
599
|
-
const
|
|
638
|
+
const buildParams = await graphBuildParams(ctx, projectPath);
|
|
639
|
+
if (!buildParams) return 1;
|
|
640
|
+
const result = await discover(projectPath, { buildParams });
|
|
600
641
|
if (result.errors.length > 0) {
|
|
601
642
|
for (const e of result.errors) console.error(formatError({ message: e.message }));
|
|
602
643
|
return 1;
|
|
@@ -5,6 +5,18 @@ import type { LexiconPlugin, ResourceMetadata } from "../../lexicon";
|
|
|
5
5
|
import type { BuildResult } from "../../build";
|
|
6
6
|
import type { ParsedArgs } from "../registry";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The aws emulator capability, as the real plugin declares it. `--live`
|
|
10
|
+
* endpoint injection reads the endpoint var off this rather than off a map
|
|
11
|
+
* keyed by lexicon name (#1345), so a mock that omits it gets no injection —
|
|
12
|
+
* the same thing that would happen in production.
|
|
13
|
+
*/
|
|
14
|
+
const awsEmulatorStub = {
|
|
15
|
+
spec: { name: "chant-floci", image: "floci/floci:1.5.34", containerPort: 4566, healthPath: "/_localstack/health" },
|
|
16
|
+
env: (endpoint: string) => ({ AWS_ENDPOINT_URL: endpoint, AWS_ACCESS_KEY_ID: "test" }),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
|
|
8
20
|
const buildMock = vi.fn();
|
|
9
21
|
const fetchLifecycleMock = vi.fn();
|
|
10
22
|
const readSnapshotMock = vi.fn();
|
|
@@ -123,6 +135,7 @@ describe("runLifecycleDiff --live", () => {
|
|
|
123
135
|
const plugins: LexiconPlugin[] = [
|
|
124
136
|
createMockPlugin({
|
|
125
137
|
name: "aws",
|
|
138
|
+
emulator: awsEmulatorStub,
|
|
126
139
|
describeResources: staticDescribeResources({
|
|
127
140
|
bucket: meta({ status: "UPDATE_COMPLETE" }),
|
|
128
141
|
}),
|
|
@@ -186,6 +199,7 @@ describe("runLifecycleDiff --live", () => {
|
|
|
186
199
|
const plugins: LexiconPlugin[] = [
|
|
187
200
|
createMockPlugin({
|
|
188
201
|
name: "aws",
|
|
202
|
+
emulator: awsEmulatorStub,
|
|
189
203
|
describeResources: async () => { throw new Error("Unable to locate credentials"); },
|
|
190
204
|
}),
|
|
191
205
|
];
|
|
@@ -211,7 +225,7 @@ describe("runLifecycleDiff --live", () => {
|
|
|
211
225
|
loadChantConfigMock.mockResolvedValue({ config: { sourceDir: "src" } });
|
|
212
226
|
|
|
213
227
|
const plugins: LexiconPlugin[] = [
|
|
214
|
-
createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
|
|
228
|
+
createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({}) }),
|
|
215
229
|
];
|
|
216
230
|
const exit = await runLifecycleDiff({
|
|
217
231
|
args: makeArgs({ path: "diff", extraPositional: "prod", extraPositional2: "aws", live: true }),
|
|
@@ -231,7 +245,7 @@ describe("runLifecycleDiff --live", () => {
|
|
|
231
245
|
loadChantConfigMock.mockResolvedValue({ config: { sourceDir: "src" } });
|
|
232
246
|
|
|
233
247
|
const plugins: LexiconPlugin[] = [
|
|
234
|
-
createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
|
|
248
|
+
createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({}) }),
|
|
235
249
|
];
|
|
236
250
|
const exit = await runLifecycleDiff({
|
|
237
251
|
args: makeArgs({ path: "diff", extraPositional: "prod", extraPositional2: "aws", live: true, src: "infra" }),
|
|
@@ -259,6 +273,7 @@ describe("runLifecycleDiff --live", () => {
|
|
|
259
273
|
const plugins: LexiconPlugin[] = [
|
|
260
274
|
createMockPlugin({
|
|
261
275
|
name: "aws",
|
|
276
|
+
emulator: awsEmulatorStub,
|
|
262
277
|
describeResources: async (options: { stack?: string }) => {
|
|
263
278
|
observedStacks.push(options.stack);
|
|
264
279
|
return {};
|
|
@@ -371,6 +386,7 @@ describe("runLifecycleDiff --live", () => {
|
|
|
371
386
|
const withDeep = (over: Parameters<typeof createMockPlugin>[0] = {}) =>
|
|
372
387
|
createMockPlugin({
|
|
373
388
|
name: "aws",
|
|
389
|
+
emulator: awsEmulatorStub,
|
|
374
390
|
describeResources: staticObservation({ bucket: meta() }),
|
|
375
391
|
observeResourcesDeep: staticDeepObservation({
|
|
376
392
|
bucket: {
|
|
@@ -501,6 +517,7 @@ describe("runLifecycleDiff --live", () => {
|
|
|
501
517
|
const plugins: LexiconPlugin[] = [
|
|
502
518
|
createMockPlugin({
|
|
503
519
|
name: "aws",
|
|
520
|
+
emulator: awsEmulatorStub,
|
|
504
521
|
describeResources: async () => {
|
|
505
522
|
seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
|
|
506
523
|
return {};
|
|
@@ -533,6 +550,7 @@ describe("runLifecycleDiff --live", () => {
|
|
|
533
550
|
const plugins: LexiconPlugin[] = [
|
|
534
551
|
createMockPlugin({
|
|
535
552
|
name: "aws",
|
|
553
|
+
emulator: awsEmulatorStub,
|
|
536
554
|
describeResources: async () => {
|
|
537
555
|
seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
|
|
538
556
|
return {};
|
|
@@ -575,7 +593,7 @@ describe("runLifecyclePlan", () => {
|
|
|
575
593
|
test("happy path: proposes a create for a declared, unobserved-nowhere-else entity", async () => {
|
|
576
594
|
buildMock.mockResolvedValue(makeBuildResult({ aws: ["bucket"] }));
|
|
577
595
|
const plugins: LexiconPlugin[] = [
|
|
578
|
-
createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
|
|
596
|
+
createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({}) }),
|
|
579
597
|
];
|
|
580
598
|
const exit = await runLifecyclePlan({
|
|
581
599
|
args: makeArgs({ path: "plan", extraPositional: "prod" }),
|
|
@@ -608,6 +626,7 @@ describe("runLifecyclePlan", () => {
|
|
|
608
626
|
const plugins: LexiconPlugin[] = [
|
|
609
627
|
createMockPlugin({
|
|
610
628
|
name: "aws",
|
|
629
|
+
emulator: awsEmulatorStub,
|
|
611
630
|
describeResources: async () => {
|
|
612
631
|
seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
|
|
613
632
|
return {};
|
|
@@ -638,6 +657,7 @@ describe("runLifecyclePlan", () => {
|
|
|
638
657
|
const plugins: LexiconPlugin[] = [
|
|
639
658
|
createMockPlugin({
|
|
640
659
|
name: "aws",
|
|
660
|
+
emulator: awsEmulatorStub,
|
|
641
661
|
describeResources: async () => {
|
|
642
662
|
seenDuringDescribe = process.env.AWS_ENDPOINT_URL;
|
|
643
663
|
return {};
|
|
@@ -720,6 +740,7 @@ describe("runLifecycleSnapshot", () => {
|
|
|
720
740
|
const plugins: LexiconPlugin[] = [
|
|
721
741
|
createMockPlugin({
|
|
722
742
|
name: "aws",
|
|
743
|
+
emulator: awsEmulatorStub,
|
|
723
744
|
describeResources: staticDescribeResources({ bucket: meta() }),
|
|
724
745
|
}),
|
|
725
746
|
];
|
|
@@ -842,7 +863,7 @@ describe("runLifecycleSnapshot", () => {
|
|
|
842
863
|
return { snapshots: [], commit: "sha", warnings: [], errors: [] };
|
|
843
864
|
});
|
|
844
865
|
const plugins: LexiconPlugin[] = [
|
|
845
|
-
createMockPlugin({ name: "aws", describeResources: staticDescribeResources({}) }),
|
|
866
|
+
createMockPlugin({ name: "aws", emulator: awsEmulatorStub, describeResources: staticDescribeResources({}) }),
|
|
846
867
|
];
|
|
847
868
|
const exit = await runLifecycleSnapshot({
|
|
848
869
|
args: makeArgs({ command: "state", path: "snapshot", extraPositional: "floci" }),
|
|
@@ -135,7 +135,7 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
|
|
|
135
135
|
// #1166 — same self-sufficiency as `chant graph --live`: a snapshot is
|
|
136
136
|
// always a live read, so an environment's declared endpoint applies here
|
|
137
137
|
// too, unless the ambient shell already set it.
|
|
138
|
-
const endpointResult = applyLiveEndpoint(config.environments, environment, observingPlugins
|
|
138
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, observingPlugins);
|
|
139
139
|
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
140
140
|
|
|
141
141
|
// Build every stack first, so the ambient scan (#1278) can be bounded by the
|
|
@@ -349,7 +349,7 @@ export async function runLifecycleDiff(ctx: CommandContext): Promise<number> {
|
|
|
349
349
|
// Floci), so `--live` is self-sufficient even when the ambient shell never
|
|
350
350
|
// exported e.g. AWS_ENDPOINT_URL. Ambient always wins when it's already set.
|
|
351
351
|
// Scoped to just the live reads below — restored in `finally`.
|
|
352
|
-
const liveLexicons = args.live ? plugins.filter((p) => p.describeResources || p.listArtifacts)
|
|
352
|
+
const liveLexicons = args.live ? plugins.filter((p) => p.describeResources || p.listArtifacts) : [];
|
|
353
353
|
const endpointResult = applyLiveEndpoint(config.environments, environment, liveLexicons);
|
|
354
354
|
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
355
355
|
|
|
@@ -597,6 +597,16 @@ async function runLifecycleDiffDigest(args: DigestDiffArgs): Promise<number> {
|
|
|
597
597
|
for (const name of diff.unchanged) {
|
|
598
598
|
console.log(name.padEnd(20) + "unchanged".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
|
|
599
599
|
}
|
|
600
|
+
|
|
601
|
+
// chant #1442 — a lexicon bump can change emitted output with no source
|
|
602
|
+
// change, so every resource above can read "unchanged" while the build is
|
|
603
|
+
// not the same build. Printed under the table rather than as a row: no
|
|
604
|
+
// resource's declaration moved, the thing that interpreted them did.
|
|
605
|
+
for (const change of diff.lexiconVersionChanges) {
|
|
606
|
+
const from = change.previous ?? "(absent)";
|
|
607
|
+
const to = change.current ?? "(absent)";
|
|
608
|
+
console.log(`\nlexicon ${change.lexicon}: ${from} → ${to}`);
|
|
609
|
+
}
|
|
600
610
|
}
|
|
601
611
|
|
|
602
612
|
return 0;
|
|
@@ -1043,7 +1053,13 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
|
|
|
1043
1053
|
// declare its own endpoint, applied here unless the ambient shell already
|
|
1044
1054
|
// set it. `chant lifecycle plan` is always a live read (no `--live` flag of
|
|
1045
1055
|
// its own), so this applies unconditionally.
|
|
1046
|
-
|
|
1056
|
+
// Names here, plugins there: the endpoint vars come from each lexicon's own
|
|
1057
|
+
// emulator capability (#1345), so this needs the loaded plugin, not the name.
|
|
1058
|
+
const endpointResult = applyLiveEndpoint(
|
|
1059
|
+
config.environments,
|
|
1060
|
+
environment,
|
|
1061
|
+
lexicons.map((name) => plugins.find((p) => p.name === name)).filter((p) => p !== undefined),
|
|
1062
|
+
);
|
|
1047
1063
|
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
1048
1064
|
|
|
1049
1065
|
try {
|
|
@@ -116,7 +116,9 @@ export function resolveProfile(
|
|
|
116
116
|
config: Record<string, unknown>,
|
|
117
117
|
profileName?: string,
|
|
118
118
|
): WorkerProfile {
|
|
119
|
-
|
|
119
|
+
// #1344 — the temporal lexicon declares this namespace and core validates it
|
|
120
|
+
// at load, so this reads a checked value rather than asserting one.
|
|
121
|
+
const temporal = (config as { temporal?: Record<string, unknown> }).temporal;
|
|
120
122
|
if (!temporal?.profiles) {
|
|
121
123
|
throw new Error(
|
|
122
124
|
'No temporal.profiles found in chant.config.ts. Add a profile to use `chant run`.',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveKindTerms, type Term } from "./search";
|
|
3
|
+
import type { IRNode } from "../../graph-ir";
|
|
4
|
+
|
|
5
|
+
const nodes = [
|
|
6
|
+
{ id: "vpc", kind: "AWS::EC2::VPC", lexicon: "aws", attrs: {} },
|
|
7
|
+
{ id: "att", kind: "AWS::EC2::VPCGatewayAttachment", lexicon: "aws", attrs: {} },
|
|
8
|
+
{ id: "sub", kind: "AWS::EC2::Subnet", lexicon: "aws", attrs: {} },
|
|
9
|
+
{ id: "assoc", kind: "AWS::EC2::SubnetRouteTableAssociation", lexicon: "aws", attrs: {} },
|
|
10
|
+
{ id: "igw", kind: "AWS::EC2::InternetGateway", lexicon: "aws", attrs: {} },
|
|
11
|
+
] as IRNode[];
|
|
12
|
+
|
|
13
|
+
const resolve = (a: string) => {
|
|
14
|
+
const t: Term = { kind: "kind", a };
|
|
15
|
+
resolveKindTerms([t], nodes);
|
|
16
|
+
return t.kinds ? [...t.kinds].sort() : undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("a kind term means the kind, not any type containing its letters", () => {
|
|
20
|
+
it("EC2::VPC is the VPC, not the gateway attachment", () => {
|
|
21
|
+
// The estate has 6 VPCs and answered 9. VPCGatewayAttachment contains the
|
|
22
|
+
// characters "EC2::VPC" and is not a VPC.
|
|
23
|
+
expect(resolve("EC2::VPC")).toEqual(["AWS::EC2::VPC"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("Subnet is the subnet, not the route table association", () => {
|
|
27
|
+
expect(resolve("Subnet")).toEqual(["AWS::EC2::Subnet"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("a full type still resolves to itself", () => {
|
|
31
|
+
expect(resolve("AWS::EC2::Subnet")).toEqual(["AWS::EC2::Subnet"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("a genuine substring search still works", () => {
|
|
35
|
+
// Nothing lines up with a `::` boundary here, so the documented substring
|
|
36
|
+
// rule applies and finds both gateway-ish kinds.
|
|
37
|
+
expect(resolve("Gateway")).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("resolves inside an edge sub-term, where the negation lives", () => {
|
|
41
|
+
const t: Term = { kind: "edge", a: "", dir: "in", sub: { kind: "kind", a: "EC2::VPC" } };
|
|
42
|
+
resolveKindTerms([t], nodes);
|
|
43
|
+
expect(t.sub?.kinds && [...t.sub.kinds]).toEqual(["AWS::EC2::VPC"]);
|
|
44
|
+
});
|
|
45
|
+
});
|