@intentius/chant 0.33.0 → 0.34.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/cli/commands/onboard.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/search.d.ts +72 -0
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +26 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/graph-effective.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +21 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-refs.d.ts +19 -0
- package/dist/graph-refs.d.ts.map +1 -1
- package/dist/lexicon.d.ts +141 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +4 -0
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/live-diff.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +55 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/replay.d.ts +47 -0
- package/dist/lifecycle/replay.d.ts.map +1 -0
- package/dist/lifecycle/snapshot.d.ts +6 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +46 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/observation.d.ts +71 -0
- package/dist/observation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/onboard.ts +10 -25
- package/src/cli/handlers/graph.test.ts +74 -0
- package/src/cli/handlers/graph.ts +77 -36
- package/src/cli/handlers/lifecycle.test.ts +86 -0
- package/src/cli/handlers/lifecycle.ts +43 -10
- package/src/cli/handlers/search.test.ts +246 -4
- package/src/cli/handlers/search.ts +432 -27
- package/src/cli/main.ts +9 -0
- package/src/cli/registry.ts +27 -0
- package/src/codegen/lexicon-wiring.test.ts +53 -0
- package/src/codegen/release-wiring.test.ts +174 -0
- package/src/graph-effective.ts +7 -1
- package/src/graph-ir-live.test.ts +83 -0
- package/src/graph-ir.ts +58 -1
- package/src/graph-refs.test.ts +59 -0
- package/src/graph-refs.ts +39 -8
- package/src/lexicon.ts +145 -0
- package/src/lifecycle/deep-observe.ts +5 -0
- package/src/lifecycle/live-diff.test.ts +38 -0
- package/src/lifecycle/live-diff.ts +45 -2
- package/src/lifecycle/observe.ts +186 -4
- package/src/lifecycle/replay.ts +141 -0
- package/src/lifecycle/snapshot.test.ts +179 -0
- package/src/lifecycle/snapshot.ts +88 -3
- package/src/lifecycle/types.ts +47 -0
- package/src/observation.test.ts +135 -0
- package/src/observation.ts +151 -0
|
@@ -62,6 +62,11 @@ interface StackTarget {
|
|
|
62
62
|
/** Build root to synthesize this stack from, scoped so its logical ids match
|
|
63
63
|
* what the stack actually deploys. */
|
|
64
64
|
root: string;
|
|
65
|
+
/** Region the stack is deployed in, from `stacks[].region` (#1261). Without
|
|
66
|
+
* it every stack is observed against the ambient region, so a multi-region
|
|
67
|
+
* estate snapshots only the stacks that happen to share it and reports the
|
|
68
|
+
* rest as "no valid resources or artifacts returned". */
|
|
69
|
+
region?: string;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
@@ -75,7 +80,7 @@ interface StackTarget {
|
|
|
75
80
|
function resolveStackTargets(args: ParsedArgs, config: ChantConfig): StackTarget[] {
|
|
76
81
|
if (args.src) return [{ root: resolve(args.src) }];
|
|
77
82
|
if (config.stacks && config.stacks.length > 0) {
|
|
78
|
-
return config.stacks.map((s) => ({ stack: s.name, root: resolve(s.src) }));
|
|
83
|
+
return config.stacks.map((s) => ({ stack: s.name, root: resolve(s.src), region: s.region }));
|
|
79
84
|
}
|
|
80
85
|
return [{ root: resolveBuildRoot(args, config) }];
|
|
81
86
|
}
|
|
@@ -133,19 +138,39 @@ export async function runLifecycleSnapshot(ctx: CommandContext): Promise<number>
|
|
|
133
138
|
const endpointResult = applyLiveEndpoint(config.environments, environment, observingPlugins.map((p) => p.name));
|
|
134
139
|
if (endpointResult.notice) console.error(formatWarning({ message: endpointResult.notice }));
|
|
135
140
|
|
|
141
|
+
// Build every stack first, so the ambient scan (#1278) can be bounded by the
|
|
142
|
+
// kinds the PROJECT manages rather than the ones this stack happens to
|
|
143
|
+
// declare. "Which of my security groups are unused" is a question about the
|
|
144
|
+
// estate; a region whose stack declares no security group still has a default
|
|
145
|
+
// one, and scoping the bound per stack silently drops it.
|
|
146
|
+
const built: Array<{ target: (typeof targets)[number]; buildResult: Awaited<ReturnType<typeof build>> }> = [];
|
|
147
|
+
for (const target of targets) {
|
|
148
|
+
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
149
|
+
const buildResult = await build(target.root, targetSerializers);
|
|
150
|
+
if (buildResult.errors.length > 0) {
|
|
151
|
+
console.error(formatError({ message: `Build failed for ${label} — fix errors before taking a snapshot` }));
|
|
152
|
+
anyHardError = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
built.push({ target, buildResult });
|
|
156
|
+
}
|
|
157
|
+
const projectKinds = [
|
|
158
|
+
...new Set(built.flatMap(({ buildResult }) => [...buildResult.entities.values()].map((e) => e.entityType))),
|
|
159
|
+
];
|
|
160
|
+
|
|
136
161
|
try {
|
|
137
|
-
for (const target of
|
|
162
|
+
for (const { target, buildResult } of built) {
|
|
138
163
|
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
139
|
-
const buildResult = await build(target.root, targetSerializers);
|
|
140
|
-
if (buildResult.errors.length > 0) {
|
|
141
|
-
console.error(formatError({ message: `Build failed for ${label} — fix errors before taking a snapshot` }));
|
|
142
|
-
anyHardError = true;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
164
|
|
|
146
165
|
let result;
|
|
147
166
|
try {
|
|
148
|
-
result = await takeSnapshot(environment, observingPlugins, buildResult, {
|
|
167
|
+
result = await takeSnapshot(environment, observingPlugins, buildResult, {
|
|
168
|
+
stack: target.stack,
|
|
169
|
+
region: target.region,
|
|
170
|
+
deep: args.deep,
|
|
171
|
+
ambient: args.ambient,
|
|
172
|
+
ambientKinds: projectKinds,
|
|
173
|
+
});
|
|
149
174
|
} catch (err) {
|
|
150
175
|
if (err instanceof StaleLifecycleBranchError) {
|
|
151
176
|
console.error(formatError({
|
|
@@ -214,7 +239,15 @@ export async function runLifecycleShow(ctx: CommandContext): Promise<number> {
|
|
|
214
239
|
|
|
215
240
|
for (const [lexicon, content] of snapshots) {
|
|
216
241
|
const snapshot: LifecycleSnapshot = JSON.parse(content);
|
|
217
|
-
|
|
242
|
+
// Depth is stated, not inferred (#1267). An identity snapshot cannot
|
|
243
|
+
// answer a property question, and a reader that assumes otherwise reads
|
|
244
|
+
// "no properties recorded" as "no such properties".
|
|
245
|
+
const depth = snapshot.depth ?? "identity";
|
|
246
|
+
const depthNote =
|
|
247
|
+
depth === "deep"
|
|
248
|
+
? ` — deep (${Object.keys(snapshot.properties ?? {}).length} property trees)`
|
|
249
|
+
: " — identity only";
|
|
250
|
+
console.log(`\n${formatBold(`${environment}/${lexicon}`)} — ${Object.keys(snapshot.resources).length} resources${depthNote} — ${snapshot.timestamp}`);
|
|
218
251
|
printSnapshotTable(snapshot);
|
|
219
252
|
}
|
|
220
253
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, test, expect, vi } from "vitest";
|
|
2
2
|
import { __searchInternals } from "./search";
|
|
3
3
|
|
|
4
|
-
const { parseQuery, matchTerm, formatRow, explain, describeTerm } = __searchInternals;
|
|
4
|
+
const { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss } = __searchInternals;
|
|
5
5
|
|
|
6
6
|
function node(id: string, kind: string, attrs: Record<string, unknown> = {}) {
|
|
7
7
|
return { id, kind, lexicon: "aws", attrs } as never;
|
|
@@ -57,9 +57,23 @@ describe("search formatting", () => {
|
|
|
57
57
|
expect(formatRow(src, [])).toBe("webServer AWS::EC2::Instance");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
test("--show
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
test("--show renders a named column whatever shape the value is", () => {
|
|
61
|
+
// A list used to be dropped silently, so `--show effectiveIngress` — the
|
|
62
|
+
// derived reachability fact — printed a blank column and read as "chant
|
|
63
|
+
// does not have this". A column the caller named is a column they get.
|
|
64
|
+
const n = node("web", "AWS::EC2::Instance", {
|
|
65
|
+
physicalId: "i-1",
|
|
66
|
+
InstanceType: "t3.micro",
|
|
67
|
+
effectiveIngress: ["tcp:22:0.0.0.0/0"],
|
|
68
|
+
});
|
|
69
|
+
expect(formatRow(n, ["InstanceType", "effectiveIngress"])).toBe(
|
|
70
|
+
'web AWS::EC2::Instance i-1 InstanceType=t3.micro effectiveIngress=["tcp:22:0.0.0.0/0"]',
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("--show omits a column the node does not carry", () => {
|
|
75
|
+
const n = node("web", "AWS::EC2::Instance", { physicalId: "i-1" });
|
|
76
|
+
expect(formatRow(n, ["VpcId"])).toBe("web AWS::EC2::Instance i-1");
|
|
63
77
|
});
|
|
64
78
|
});
|
|
65
79
|
|
|
@@ -111,3 +125,231 @@ describe("search edge traversal", () => {
|
|
|
111
125
|
.toBe("→attr:MapPublicIpOnLaunch=true (no such edge)");
|
|
112
126
|
});
|
|
113
127
|
});
|
|
128
|
+
|
|
129
|
+
describe("search surfaces what the graph derived", () => {
|
|
130
|
+
const insts = [
|
|
131
|
+
node("webServer", "AWS::EC2::Instance", { internetFacing: true, internetFacingVia: "rtb-1 → igw-1", effectiveIngress: ["tcp:22:0.0.0.0/0"] }),
|
|
132
|
+
node("privServer", "AWS::EC2::Instance", { internetFacing: false, effectiveIngress: [] }),
|
|
133
|
+
];
|
|
134
|
+
const derivedIr = { nodes: insts, edges: [], groups: {}, derivedAttrs: { Instance: ["internetFacing", "effectiveIngress"] } } as never;
|
|
135
|
+
|
|
136
|
+
function capture(fn: () => void): string {
|
|
137
|
+
const lines: string[] = [];
|
|
138
|
+
const spy = vi.spyOn(console, "log").mockImplementation((s: string) => { lines.push(s); });
|
|
139
|
+
fn();
|
|
140
|
+
spy.mockRestore();
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
test("names derived facts the query did not use, and omits the ones it did", () => {
|
|
145
|
+
const out = capture(() => derivedSurface(parseQuery("kind:EC2::Instance attr:internetFacing=true") as never, insts as never, derivedIr));
|
|
146
|
+
expect(out).toContain("effectiveIngress");
|
|
147
|
+
expect(out).not.toContain("internetFacing");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("says nothing when the query already used every derived fact", () => {
|
|
151
|
+
const q = "kind:EC2::Instance attr:internetFacing=true attr:effectiveIngress=tcp:22:0.0.0.0/0";
|
|
152
|
+
expect(capture(() => derivedSurface(parseQuery(q) as never, insts as never, derivedIr))).toBe("");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("says nothing for a graph with no derived facts recorded", () => {
|
|
156
|
+
const plain = { nodes: insts, edges: [], groups: {} } as never;
|
|
157
|
+
expect(capture(() => derivedSurface(parseQuery("kind:EC2::Instance") as never, insts as never, plain))).toBe("");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("a miss lists the attributes the queried kind actually carries", () => {
|
|
161
|
+
const out = capture(() => availableAttrs(parseQuery("kind:EC2::Instance attr:nosuchattr=1") as never, derivedIr));
|
|
162
|
+
expect(out).toContain("effectiveIngress");
|
|
163
|
+
expect(out).toContain("internetFacing");
|
|
164
|
+
expect(out).not.toContain("nosuchattr");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("inclusion evidence is keyed off <attr>Via provenance, not a fixed attribute name", () => {
|
|
168
|
+
const byId = new Map(insts.map((n: { id: string }) => [n.id, n]));
|
|
169
|
+
const q = "attr:internetFacing=true";
|
|
170
|
+
const out = capture(() => explain(parseQuery(q) as never, [insts[0]] as never, derivedIr, byId as never, q));
|
|
171
|
+
expect(out).toContain("webServer internetFacing via rtb-1 → igw-1");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// #1280 — absence is a real estate question ("what does nothing reference"),
|
|
176
|
+
// and the grammar could only express presence.
|
|
177
|
+
describe("negated terms (#1280)", () => {
|
|
178
|
+
const ir = {
|
|
179
|
+
nodes: [
|
|
180
|
+
{ id: "used", kind: "AWS::EC2::SecurityGroup", lexicon: "aws", attrs: {} },
|
|
181
|
+
{ id: "spare", kind: "AWS::EC2::SecurityGroup", lexicon: "aws", attrs: {} },
|
|
182
|
+
{ id: "web", kind: "AWS::EC2::Instance", lexicon: "aws", attrs: {} },
|
|
183
|
+
],
|
|
184
|
+
edges: [{ from: "web", to: "used", kind: "ref" as const, viaAttr: "SecurityGroupIds" }],
|
|
185
|
+
groups: {},
|
|
186
|
+
};
|
|
187
|
+
const byId = new Map(ir.nodes.map((n) => [n.id, n]));
|
|
188
|
+
const match = (q: string) =>
|
|
189
|
+
ir.nodes.filter((n) => parseQuery(q).every((t) => matchTerm(n, t, ir, byId))).map((n) => n.id);
|
|
190
|
+
|
|
191
|
+
test("selects what nothing references — the complement of an edge term", () => {
|
|
192
|
+
expect(match("kind:SecurityGroup !<-kind:EC2::Instance")).toEqual(["spare"]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("the un-negated query still selects what IS referenced", () => {
|
|
196
|
+
expect(match("kind:SecurityGroup <-kind:EC2::Instance")).toEqual(["used"]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("negates a plain attribute term too", () => {
|
|
200
|
+
const nodes = [
|
|
201
|
+
{ id: "a", kind: "K", lexicon: "x", attrs: { env: "prod" } },
|
|
202
|
+
{ id: "b", kind: "K", lexicon: "x", attrs: { env: "dev" } },
|
|
203
|
+
];
|
|
204
|
+
const g = { nodes, edges: [], groups: {} };
|
|
205
|
+
const m = new Map(nodes.map((n) => [n.id, n]));
|
|
206
|
+
expect(
|
|
207
|
+
nodes.filter((n) => parseQuery("!attr:env=prod").every((t) => matchTerm(n, t, g, m))).map((n) => n.id),
|
|
208
|
+
).toEqual(["b"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("a bare edge term is refused, and the refusal names the correction", () => {
|
|
212
|
+
// Accepting it as "no edge in this direction" is coherent and still the
|
|
213
|
+
// wrong query for "what is unused": it counts every reference, including a
|
|
214
|
+
// stack output that merely publishes a resource's id, so it omits the very
|
|
215
|
+
// group the question is about. Measured — refused: 3/3 right; accepted:
|
|
216
|
+
// wrong in 2 runs of 3.
|
|
217
|
+
expect(() => parseQuery("kind:Foo !<-")).toThrow(/needs a target/);
|
|
218
|
+
expect(() => parseQuery("kind:Foo ->")).toThrow(/needs a target/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("a made-up prefix is refused, and names the correction", () => {
|
|
222
|
+
// An agent looking for SSH reachability wrote
|
|
223
|
+
// `effectiveIngress:tcp:22:0.0.0.0/0` — right idea, right attribute, wrong
|
|
224
|
+
// spelling — and this parsed as a free-text word that matched nothing. It
|
|
225
|
+
// read the clean empty result as "chant does not hold this fact" and
|
|
226
|
+
// rebuilt the answer by hand from security-group rows.
|
|
227
|
+
expect(() => parseQuery("kind:EC2::Instance effectiveIngress:tcp:22")).toThrow(
|
|
228
|
+
/there is no "effectiveIngress:" prefix/,
|
|
229
|
+
);
|
|
230
|
+
try {
|
|
231
|
+
parseQuery("effectiveIngress:tcp:22");
|
|
232
|
+
} catch (e) {
|
|
233
|
+
expect((e as { hint: string }).hint).toContain("attr:effectiveIngress=tcp:22");
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("still accepts a word that merely contains colons", () => {
|
|
238
|
+
// `AWS::EC2::Instance` and a URL are words, not malformed terms — a real
|
|
239
|
+
// prefix is one colon, not two.
|
|
240
|
+
expect(parseQuery("AWS::EC2::Instance")).toEqual([{ kind: "word", a: "AWS::EC2::Instance" }]);
|
|
241
|
+
expect(parseQuery("https://example.com")).toEqual([{ kind: "word", a: "https://example.com" }]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("an edge term WITH a target still parses", () => {
|
|
245
|
+
expect(() => parseQuery("kind:Foo !<-kind:Bar")).not.toThrow();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("--explain says the term was negated, or an exclusion reads inverted", () => {
|
|
249
|
+
expect(describeTerm(parseQuery("!kind:Foo")[0])).toBe("!kind:Foo");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// #1278/#1279 — `--ambient` changes what a LIVE read goes and looks for. On a
|
|
254
|
+
// replay it changes nothing: what is ambient in a recording was fixed when the
|
|
255
|
+
// recording was taken.
|
|
256
|
+
describe("the --ambient hint", () => {
|
|
257
|
+
const sg = node("sg-1", "AWS::EC2::SecurityGroup");
|
|
258
|
+
const kinds = ["AWS::EC2::SecurityGroup"];
|
|
259
|
+
const capture = (fn: () => void): string => {
|
|
260
|
+
const lines: string[] = [];
|
|
261
|
+
const spy = vi.spyOn(console, "log").mockImplementation((s: string) => { lines.push(s); });
|
|
262
|
+
fn();
|
|
263
|
+
spy.mockRestore();
|
|
264
|
+
return lines.join("\n");
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
test("names the flag on a live read that did not use it", () => {
|
|
268
|
+
expect(capture(() => ambientHint([sg] as never, kinds, false))).toContain("--ambient");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("says nothing when the caller already asked for it", () => {
|
|
272
|
+
expect(capture(() => ambientHint([sg] as never, kinds, true))).toBe("");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("says nothing on a replay whose snapshot already holds ambient resources", () => {
|
|
276
|
+
// The answer is complete. Saying the flag would add something is worse than
|
|
277
|
+
// silence: an agent read "6 of 6 matched" next to this hint, went looking
|
|
278
|
+
// for a seventh group, and hand-built a wrong answer from the raw graph.
|
|
279
|
+
expect(capture(() => ambientHint([sg] as never, kinds, false, { recordedAmbient: true }))).toBe("");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("on a replay without them, points at the recording rather than the query", () => {
|
|
283
|
+
// `--at --ambient` cannot go and look; only a new snapshot can.
|
|
284
|
+
const out = capture(() => ambientHint([sg] as never, kinds, false, { recordedAmbient: false }));
|
|
285
|
+
expect(out).toContain("lifecycle snapshot");
|
|
286
|
+
expect(out).not.toContain("--ambient includes those");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// #1279 — asked to list instances "in all regions", an agent printed six
|
|
291
|
+
// correct ids with no region against any of them, and was judged wrong.
|
|
292
|
+
describe("the region spread of an answer", () => {
|
|
293
|
+
const inst = (id: string, region?: string) =>
|
|
294
|
+
node(id, "AWS::EC2::Instance", region ? { region } : {});
|
|
295
|
+
const capture = (fn: () => void): string => {
|
|
296
|
+
const lines: string[] = [];
|
|
297
|
+
const spy = vi.spyOn(console, "log").mockImplementation((s: string) => { lines.push(s); });
|
|
298
|
+
fn();
|
|
299
|
+
spy.mockRestore();
|
|
300
|
+
return lines.join("\n");
|
|
301
|
+
};
|
|
302
|
+
const spread = (ns: unknown[], show: string[] = [], q = "kind:EC2::Instance") =>
|
|
303
|
+
capture(() => regionSpread(parseQuery(q) as never, ns as never, show));
|
|
304
|
+
|
|
305
|
+
test("names the regions when the answer spans several", () => {
|
|
306
|
+
const out = spread([inst("a", "us-east-1"), inst("b", "us-west-2")]);
|
|
307
|
+
expect(out).toContain("us-east-1, us-west-2");
|
|
308
|
+
expect(out).toContain("2 regions");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("says nothing when everything is in one region", () => {
|
|
312
|
+
expect(spread([inst("a", "us-east-1"), inst("b", "us-east-1")])).toBe("");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("says nothing when the caller already asked for region", () => {
|
|
316
|
+
expect(spread([inst("a", "us-east-1"), inst("b", "us-west-2")], ["region"])).toBe("");
|
|
317
|
+
expect(spread([inst("a", "us-east-1"), inst("b", "us-west-2")], [], "attr:region=us-east-1")).toBe("");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("says nothing when the resources carry no region", () => {
|
|
321
|
+
expect(spread([inst("a"), inst("b")])).toBe("");
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// #1279 — seven `--show` names in one benchmark run missed on case alone, and
|
|
326
|
+
// a missed name printed nothing at all rather than saying it had missed.
|
|
327
|
+
describe("--show name matching", () => {
|
|
328
|
+
const n = node("web", "AWS::EC2::Instance", { physicalId: "i-1", region: "us-east-1" });
|
|
329
|
+
const capture = (fn: () => void): string => {
|
|
330
|
+
const lines: string[] = [];
|
|
331
|
+
const spy = vi.spyOn(console, "log").mockImplementation((s: string) => { lines.push(s); });
|
|
332
|
+
fn();
|
|
333
|
+
spy.mockRestore();
|
|
334
|
+
return lines.join("\n");
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
test("matches a column name whatever case the caller used", () => {
|
|
338
|
+
// AWS names are PascalCase and chant's derived ones are not, so a caller
|
|
339
|
+
// mixing them is normal. Both are the same request.
|
|
340
|
+
expect(formatRow(n, ["Region"])).toContain("region=us-east-1");
|
|
341
|
+
expect(formatRow(n, ["region"])).toContain("region=us-east-1");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("prints the name the resource actually uses, not the one asked for", () => {
|
|
345
|
+
expect(formatRow(n, ["REGION"])).toContain("region=us-east-1");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("says when nothing carries a requested column", () => {
|
|
349
|
+
expect(capture(() => showMiss([n] as never, ["VpcId"]))).toContain("VpcId");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("says nothing when every requested column is present", () => {
|
|
353
|
+
expect(capture(() => showMiss([n] as never, ["Region"]))).toBe("");
|
|
354
|
+
});
|
|
355
|
+
});
|