@intentius/chant 0.124.45 → 0.124.46
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/capabilities/aws.paths +148 -0
- package/dist/capabilities/base-mappings.json +122 -0
- package/dist/capabilities/catalog.d.ts +89 -0
- package/dist/capabilities/catalog.d.ts.map +1 -0
- package/dist/capabilities/catalog.js +204 -0
- package/dist/capabilities/language.d.ts +103 -0
- package/dist/capabilities/language.d.ts.map +1 -0
- package/dist/capabilities/language.js +335 -0
- package/dist/capabilities/matcher.d.ts +65 -0
- package/dist/capabilities/matcher.d.ts.map +1 -0
- package/dist/capabilities/matcher.js +299 -0
- package/dist/capabilities/view.d.ts +207 -0
- package/dist/capabilities/view.d.ts.map +1 -0
- package/dist/capabilities/view.js +484 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +1 -0
- package/dist/cli/handlers/declared-graph.d.ts +44 -0
- package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
- package/dist/cli/handlers/declared-graph.js +87 -0
- package/dist/cli/handlers/graph.d.ts +25 -0
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +93 -87
- package/dist/cli/handlers/pr-env.d.ts.map +1 -1
- package/dist/cli/handlers/pr-env.js +2 -1
- package/dist/cli/handlers/search-count.d.ts +89 -0
- package/dist/cli/handlers/search-count.d.ts.map +1 -0
- package/dist/cli/handlers/search-count.js +132 -0
- package/dist/cli/handlers/search.d.ts +358 -6
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/handlers/search.js +1065 -155
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +23 -3
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +3 -0
- package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
- package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
- package/dist/cli/mcp/tools/capabilities.js +27 -0
- package/dist/cli/mcp/workspace-tools.d.ts +3 -1
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.js +47 -2
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +64 -17
- package/dist/cli/registry.d.ts +22 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/pr-env-ports.d.ts +29 -0
- package/dist/components/pr-env-ports.d.ts.map +1 -0
- package/dist/components/pr-env-ports.js +77 -0
- package/dist/components/pr-env-run.d.ts +5 -21
- package/dist/components/pr-env-run.d.ts.map +1 -1
- package/dist/components/pr-env-run.js +6 -70
- package/dist/components/pr-environment.d.ts +2 -1
- package/dist/components/pr-environment.d.ts.map +1 -1
- package/dist/components/pr-environment.js +1 -1
- package/dist/config-environments.d.ts +92 -0
- package/dist/config-environments.d.ts.map +1 -0
- package/dist/config-environments.js +102 -0
- package/dist/config.d.ts +24 -84
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -96
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +15 -3
- package/dist/graph-effective.d.ts +33 -2
- package/dist/graph-effective.d.ts.map +1 -1
- package/dist/graph-effective.js +215 -68
- package/dist/graph-ir.d.ts +23 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +130 -0
- package/dist/graph-sub-template.d.ts +11 -0
- package/dist/graph-sub-template.d.ts.map +1 -0
- package/dist/graph-sub-template.js +19 -0
- package/dist/lexicon.d.ts +9 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts +28 -0
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.js +23 -1
- package/dist/lint/rule.d.ts +6 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/live-endpoint.d.ts +1 -1
- package/dist/live-endpoint.d.ts.map +1 -1
- package/dist/live-endpoint.js +1 -1
- package/dist/observation.d.ts +48 -1
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +21 -5
- package/dist/op/activities/activity-contracts.d.ts +4 -4
- package/dist/wave-runtime.mjs +250 -249
- package/dist/workspace/capabilities-contract.d.ts +66 -0
- package/dist/workspace/capabilities-contract.d.ts.map +1 -0
- package/dist/workspace/capabilities-contract.js +107 -0
- package/dist/workspace/capabilities.schema.json +229 -0
- package/dist/workspace/compose-graph.d.ts +9 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +3 -1
- package/dist/workspace/conformance/index.d.ts +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/index.js +39 -2
- package/dist/workspace/graph-cache.d.ts +8 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -1
- package/dist/workspace/graph-cache.js +27 -3
- package/dist/workspace/graph-cli.d.ts +39 -2
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +66 -24
- package/dist/workspace/graph.schema.json +148 -0
- package/dist/workspace/kind-readers.d.ts +3 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -1
- package/dist/workspace/kind-readers.js +7 -1
- package/dist/workspace/member-commands.d.ts +10 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-commands.js +17 -1
- package/dist/workspace/nested-graph.d.ts +2 -0
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +32 -1
- package/dist/workspace/reason-codes.d.ts +10 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +12 -0
- package/dist/workspace/search-cli.d.ts +204 -0
- package/dist/workspace/search-cli.d.ts.map +1 -0
- package/dist/workspace/search-cli.js +466 -0
- package/dist/workspace/search-contract.d.ts +56 -0
- package/dist/workspace/search-contract.d.ts.map +1 -0
- package/dist/workspace/search-contract.js +70 -0
- package/dist/workspace/search.schema.json +822 -0
- package/package.json +1 -1
- package/src/__snapshots__/okf.test.ts.snap +10 -0
- package/src/capabilities/aws.paths +148 -0
- package/src/capabilities/base-mappings.json +122 -0
- package/src/capabilities/catalog.ts +230 -0
- package/src/capabilities/language.test.ts +191 -0
- package/src/capabilities/language.ts +373 -0
- package/src/capabilities/matcher.ts +315 -0
- package/src/capabilities/view-compose.test.ts +76 -0
- package/src/capabilities/view.ts +631 -0
- package/src/cli/commands/lint.ts +1 -0
- package/src/cli/handlers/declared-graph.ts +99 -0
- package/src/cli/handlers/graph.test.ts +94 -3
- package/src/cli/handlers/graph.ts +123 -96
- package/src/cli/handlers/pr-env.ts +1 -2
- package/src/cli/handlers/search-build-roots.test.ts +144 -0
- package/src/cli/handlers/search-count.test.ts +191 -0
- package/src/cli/handlers/search-count.ts +165 -0
- package/src/cli/handlers/search-json.test.ts +333 -0
- package/src/cli/handlers/search-live.test.ts +387 -0
- package/src/cli/handlers/search-paths.test.ts +235 -0
- package/src/cli/handlers/search.ts +1312 -166
- package/src/cli/main.test.ts +28 -3
- package/src/cli/main.ts +23 -3
- package/src/cli/mcp/server.test.ts +3 -3
- package/src/cli/mcp/server.ts +3 -0
- package/src/cli/mcp/tools/capabilities.ts +32 -0
- package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
- package/src/cli/mcp/workspace-tools.ts +47 -2
- package/src/cli/parse-args.ts +52 -13
- package/src/cli/registry.ts +22 -0
- package/src/components/pr-env-ports.ts +89 -0
- package/src/components/pr-env-run-bundle.test.ts +54 -0
- package/src/components/pr-env-run.ts +5 -76
- package/src/components/pr-environment.ts +2 -1
- package/src/config-environments.ts +133 -0
- package/src/config.ts +37 -124
- package/src/graph-detail.ts +15 -3
- package/src/graph-effective.ts +226 -75
- package/src/graph-ir.test.ts +163 -0
- package/src/graph-ir.ts +147 -0
- package/src/graph-sub-template.ts +22 -0
- package/src/lexicon.ts +10 -0
- package/src/lifecycle/observe.test.ts +27 -1
- package/src/lifecycle/observe.ts +43 -1
- package/src/lint/rule.ts +6 -0
- package/src/live-endpoint.ts +1 -1
- package/src/observation.test.ts +25 -0
- package/src/observation.ts +69 -5
- package/src/workspace/capabilities-contract.test.ts +88 -0
- package/src/workspace/capabilities-contract.ts +132 -0
- package/src/workspace/capabilities.schema.json +229 -0
- package/src/workspace/compose-graph.ts +10 -1
- package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
- package/src/workspace/conformance/index.ts +31 -2
- package/src/workspace/graph-cache.ts +27 -3
- package/src/workspace/graph-cli.ts +96 -24
- package/src/workspace/graph.schema.json +148 -0
- package/src/workspace/kind-readers.e2e.test.ts +28 -0
- package/src/workspace/kind-readers.test.ts +1 -1
- package/src/workspace/kind-readers.ts +7 -1
- package/src/workspace/member-commands.ts +22 -1
- package/src/workspace/nested-graph.ts +32 -1
- package/src/workspace/reason-codes.test.ts +7 -0
- package/src/workspace/reason-codes.ts +12 -0
- package/src/workspace/search-cli.e2e.test.ts +305 -0
- package/src/workspace/search-cli.ts +591 -0
- package/src/workspace/search-contract.ts +95 -0
- package/src/workspace/search.schema.json +822 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { ParsedArgs } from "../registry";
|
|
3
|
+
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
4
|
+
import { createMockPlugin } from "../../../../test-utils/src/mock-plugin";
|
|
5
|
+
import { parseArgs } from "../parse-args";
|
|
6
|
+
import { contract } from "../../workspace/__fixtures__/contract-repo";
|
|
7
|
+
import searchSchema from "../../workspace/search.schema.json";
|
|
8
|
+
import { SEARCH_ERROR_CODES, SEARCH_OUTPUT_SCHEMA_ID } from "../../workspace/search-contract";
|
|
9
|
+
import { readChantPackageVersion } from "../../package-version";
|
|
10
|
+
|
|
11
|
+
// #4374: every document is the read contract's `search` document, and is held to its schema.
|
|
12
|
+
const { expectValid } = contract(searchSchema);
|
|
13
|
+
const HEAD = { $schema: SEARCH_OUTPUT_SCHEMA_ID, contract: 1, chant: readChantPackageVersion() };
|
|
14
|
+
|
|
15
|
+
// #4367: `chant search --json` is the whole answer as one document on stdout:
|
|
16
|
+
// full node ids, displayId, show, provenance, explain, drift, live failures
|
|
17
|
+
// and refusals. #4369: a query that opens with `->` reaches the handler.
|
|
18
|
+
|
|
19
|
+
const discoverMock = vi.fn();
|
|
20
|
+
vi.mock("../../discovery/index", () => ({
|
|
21
|
+
discover: (...a: unknown[]) => discoverMock(...a),
|
|
22
|
+
}));
|
|
23
|
+
const loadPluginsMock = vi.fn();
|
|
24
|
+
const resolveLexMock = vi.fn();
|
|
25
|
+
vi.mock("../plugins", async () => {
|
|
26
|
+
const actual = await vi.importActual<typeof import("../plugins")>("../plugins");
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
loadPlugins: (...a: unknown[]) => loadPluginsMock(...a),
|
|
30
|
+
resolveProjectLexicons: (...a: unknown[]) => resolveLexMock(...a),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
const replaySnapshotsMock = vi.fn();
|
|
34
|
+
const hasSnapshotMock = vi.fn((..._a: unknown[]) => Promise.resolve(false));
|
|
35
|
+
vi.mock("../../lifecycle/replay", () => ({
|
|
36
|
+
replaySnapshots: (...a: unknown[]) => replaySnapshotsMock(...a),
|
|
37
|
+
hasSnapshot: (...a: unknown[]) => hasSnapshotMock(...a),
|
|
38
|
+
}));
|
|
39
|
+
const loadChantConfigMock = vi.fn();
|
|
40
|
+
vi.mock("../../config", async () => {
|
|
41
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
42
|
+
return { ...actual, loadChantConfig: (...a: unknown[]) => loadChantConfigMock(...a) };
|
|
43
|
+
});
|
|
44
|
+
const buildResultMock = vi.fn();
|
|
45
|
+
vi.mock("../../build", async () => {
|
|
46
|
+
const actual = await vi.importActual<typeof import("../../build")>("../../build");
|
|
47
|
+
return {
|
|
48
|
+
...actual,
|
|
49
|
+
build: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)),
|
|
50
|
+
buildProject: (...a: unknown[]) => Promise.resolve(buildResultMock(...a)),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const { runSearch } = await import("./search");
|
|
55
|
+
|
|
56
|
+
function decl<T extends object>(base: T): Declarable & T {
|
|
57
|
+
return { [DECLARABLE_MARKER]: true, ...base } as Declarable & T;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeArgs(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
61
|
+
return {
|
|
62
|
+
command: "search", path: "kind:Instance",
|
|
63
|
+
format: "", fix: false, watch: false, verbose: false, help: false, live: false, json: true,
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const instance = (props: Record<string, unknown> = {}) => decl({ lexicon: "aws", entityType: "AWS::EC2::Instance", props });
|
|
69
|
+
const entities = new Map<string, Declarable>([
|
|
70
|
+
["webServer", instance({ InstanceType: "t3.micro" })],
|
|
71
|
+
["privateServer", instance({ InstanceType: "t3.large" })],
|
|
72
|
+
["bucket", decl({ lexicon: "aws", entityType: "AWS::S3::Bucket", props: {} })],
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
let out: string[];
|
|
76
|
+
let err: string[];
|
|
77
|
+
|
|
78
|
+
async function search(args: Partial<ParsedArgs>): Promise<{ exit: number; doc: ReturnType<typeof JSON.parse> }> {
|
|
79
|
+
out = [];
|
|
80
|
+
const exit = await runSearch({ args: makeArgs(args), plugins: [], serializers: [] });
|
|
81
|
+
// One document, and nothing else on stdout.
|
|
82
|
+
expect(out).toHaveLength(1);
|
|
83
|
+
const doc = JSON.parse(out[0]);
|
|
84
|
+
expectValid(doc);
|
|
85
|
+
// A refusal's code is from the contract's closed list.
|
|
86
|
+
if (doc.error) expect(SEARCH_ERROR_CODES).toContain(doc.error.code);
|
|
87
|
+
return { exit, doc };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
out = [];
|
|
92
|
+
err = [];
|
|
93
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { out.push(s); });
|
|
94
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { err.push(s); });
|
|
95
|
+
discoverMock.mockReset();
|
|
96
|
+
discoverMock.mockResolvedValue({ entities, errors: [], sourceFiles: [] });
|
|
97
|
+
buildResultMock.mockReset();
|
|
98
|
+
buildResultMock.mockReturnValue({ errors: [], entities, outputs: new Map([["aws", ""]]), warnings: [] });
|
|
99
|
+
loadChantConfigMock.mockReset();
|
|
100
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
101
|
+
resolveLexMock.mockReset();
|
|
102
|
+
resolveLexMock.mockResolvedValue(["aws"]);
|
|
103
|
+
loadPluginsMock.mockReset();
|
|
104
|
+
loadPluginsMock.mockResolvedValue([]);
|
|
105
|
+
hasSnapshotMock.mockReset();
|
|
106
|
+
hasSnapshotMock.mockResolvedValue(false);
|
|
107
|
+
replaySnapshotsMock.mockReset();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("search --json on two stacks with the same name (#4367)", () => {
|
|
111
|
+
test("rows carry distinct stack-qualified ids, where the text rows print the same name twice", async () => {
|
|
112
|
+
loadChantConfigMock.mockResolvedValue({ config: { stacks: [{ name: "east", src: "east" }, { name: "west", src: "west" }] } });
|
|
113
|
+
discoverMock.mockImplementation(async (path: string) => ({
|
|
114
|
+
entities: new Map([["web", instance()]]),
|
|
115
|
+
errors: [],
|
|
116
|
+
sourceFiles: [path],
|
|
117
|
+
}));
|
|
118
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
119
|
+
observations: [{
|
|
120
|
+
lexicon: "aws",
|
|
121
|
+
resources: {
|
|
122
|
+
"east::web": { type: "AWS::EC2::Instance", physicalId: "i-east", status: "OK" },
|
|
123
|
+
"west::web": { type: "AWS::EC2::Instance", physicalId: "i-west", status: "OK" },
|
|
124
|
+
},
|
|
125
|
+
}],
|
|
126
|
+
commit: "a1b2c3d4e5f6", timestamp: "2026-08-01T03:15:00.000Z", depth: "identity",
|
|
127
|
+
});
|
|
128
|
+
loadPluginsMock.mockResolvedValue([createMockPlugin({ name: "aws", describeResources: async () => ({}) })]);
|
|
129
|
+
const { exit, doc } = await search({ path: "kind:EC2::Instance", at: "latest", env: "prod" });
|
|
130
|
+
expect(exit).toBe(0);
|
|
131
|
+
expect(doc.source).toEqual({ kind: "snapshot", snapshot: "latest", commit: "a1b2c3d4e5f6", timestamp: "2026-08-01T03:15:00.000Z", env: "prod" });
|
|
132
|
+
expect(doc).toMatchObject({ ...HEAD, at: null });
|
|
133
|
+
expect(doc.matches.map((m: { id: string }) => m.id).sort()).toEqual(["east::web", "west::web"]);
|
|
134
|
+
expect(doc.matches.map((m: { displayId: string }) => m.displayId)).toEqual(["web", "web"]);
|
|
135
|
+
expect(doc.matches.map((m: { physicalId: string }) => m.physicalId).sort()).toEqual(["i-east", "i-west"]);
|
|
136
|
+
expect(doc.provenance).toBe("observed from snapshot a1b2c3d taken 2026-08-01T03:15:00.000Z · bound 2/2");
|
|
137
|
+
|
|
138
|
+
// The text rows are the bare name, which is why the ids are needed.
|
|
139
|
+
out = [];
|
|
140
|
+
await runSearch({ args: makeArgs({ path: "kind:EC2::Instance", at: "latest", env: "prod", json: undefined }), plugins: [], serializers: [] });
|
|
141
|
+
expect(out.filter((l) => l.startsWith("web "))).toEqual(["web AWS::EC2::Instance i-east", "web AWS::EC2::Instance i-west"]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("search --json is the read contract's search document (#4374)", () => {
|
|
146
|
+
test("a result: the head, at null, and the workspace the project is in (here the chant repository's own)", async () => {
|
|
147
|
+
const { doc } = await search({ path: "kind:EC2::Instance" });
|
|
148
|
+
expect(doc).toMatchObject({ ...HEAD, at: null, query: "kind:EC2::Instance", workspace: { root: "." } });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a --count-by result is the counts branch: countBy, groups and total, no matches", async () => {
|
|
152
|
+
const { exit, doc } = await search({ path: "", countBy: "kind" });
|
|
153
|
+
expect(exit).toBe(0);
|
|
154
|
+
expect(doc).toMatchObject({ ...HEAD, at: null, countBy: "kind", total: 3 });
|
|
155
|
+
expect(doc.groups.map(({ group, count }: { group: string; count: number }) => ({ group, count }))).toEqual([{ group: "AWS::EC2::Instance", count: 2 }, { group: "AWS::S3::Bucket", count: 1 }]);
|
|
156
|
+
// #4382: each group names its matches.
|
|
157
|
+
for (const g of doc.groups as Array<{ count: number; ids: string[] }>) expect(g.ids).toHaveLength(g.count);
|
|
158
|
+
expect(doc).not.toHaveProperty("matches");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("--snapshot <ref> is --at <ref> for plain search, and source.snapshot names it", async () => {
|
|
162
|
+
replaySnapshotsMock.mockResolvedValue({
|
|
163
|
+
observations: [{ lexicon: "aws", resources: { webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK" } } }],
|
|
164
|
+
commit: "a1b2c3d4e5f6", timestamp: "2026-08-01T03:15:00.000Z", depth: "identity",
|
|
165
|
+
});
|
|
166
|
+
loadPluginsMock.mockResolvedValue([createMockPlugin({ name: "aws", describeResources: async () => ({}) })]);
|
|
167
|
+
const viaSnapshot = await search({ path: "kind:EC2::Instance", snapshot: "latest", env: "dev" });
|
|
168
|
+
const viaAt = await search({ path: "kind:EC2::Instance", at: "latest", env: "dev" });
|
|
169
|
+
expect(viaSnapshot.doc).toEqual(viaAt.doc);
|
|
170
|
+
expect(viaSnapshot.doc.source).toMatchObject({ kind: "snapshot", snapshot: "latest", commit: "a1b2c3d4e5f6" });
|
|
171
|
+
expect(replaySnapshotsMock.mock.calls.map((c) => c[1])).toEqual(["latest", "latest"]);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("search --json carries the text footers as fields (#4367)", () => {
|
|
176
|
+
test("declared: displayId, show, provenance; no env on a declared source", async () => {
|
|
177
|
+
const { exit, doc } = await search({ path: "kind:EC2::Instance", show: "instancetype,Missing" });
|
|
178
|
+
expect(exit).toBe(0);
|
|
179
|
+
expect(doc.source).toEqual({ kind: "declared" });
|
|
180
|
+
expect([...doc.matches].sort((x, y) => x.id.localeCompare(y.id))).toEqual([
|
|
181
|
+
{ id: "privateServer", displayId: "privateServer", kind: "AWS::EC2::Instance", show: { InstanceType: "t3.large" }, paths: [] },
|
|
182
|
+
{ id: "webServer", displayId: "webServer", kind: "AWS::EC2::Instance", show: { InstanceType: "t3.micro" }, paths: [] },
|
|
183
|
+
]);
|
|
184
|
+
expect(doc.provenance).toBe("declared only · no observation · physical ids unavailable");
|
|
185
|
+
expect(doc).not.toHaveProperty("explain");
|
|
186
|
+
expect(doc).not.toHaveProperty("drift");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("--explain: universe, near-misses by the term they fail", async () => {
|
|
190
|
+
const { doc } = await search({ path: "kind:EC2::Instance attr:InstanceType=micro", explain: true });
|
|
191
|
+
expect(doc.matches.map((m: { id: string }) => m.id)).toEqual(["webServer"]);
|
|
192
|
+
expect(doc.explain).toEqual({
|
|
193
|
+
universe: { matched: 1, of: 2, kinds: ["AWS::EC2::Instance"] },
|
|
194
|
+
evidence: [],
|
|
195
|
+
nearMisses: [{ id: "privateServer", fails: "attr:InstanceType=micro" }],
|
|
196
|
+
moreExcluded: 0,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("no matches is a document with an empty list, not \"(no matches)\"", async () => {
|
|
201
|
+
const { exit, doc } = await search({ path: "kind:Lambda", explain: true });
|
|
202
|
+
expect(exit).toBe(0);
|
|
203
|
+
expect(doc.matches).toEqual([]);
|
|
204
|
+
expect(doc.explain.universe).toEqual({ matched: 0, of: 3, kinds: [] });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("a failed live read: liveFailures, each row unobserved, exit 1", async () => {
|
|
208
|
+
loadPluginsMock.mockResolvedValue([
|
|
209
|
+
createMockPlugin({ name: "aws", describeResources: async () => { throw new Error("connect ECONNREFUSED 127.0.0.1:9999"); } }),
|
|
210
|
+
]);
|
|
211
|
+
const { exit, doc } = await search({ path: "kind:EC2::Instance", live: true, env: "dev" });
|
|
212
|
+
expect(exit).toBe(1);
|
|
213
|
+
expect(doc.source).toEqual({ kind: "live", env: "dev" });
|
|
214
|
+
expect(doc.liveFailures).toHaveLength(1);
|
|
215
|
+
expect(doc.liveFailures[0]).toContain("ECONNREFUSED");
|
|
216
|
+
expect(doc.matches.map((m: { unobserved: string }) => m.unobserved)).toEqual(["read-failed", "read-failed"]);
|
|
217
|
+
expect(doc.provenance).toBe("live read failed (aws) · 2/2 rows unobserved · answered from the declared graph · physical ids unavailable");
|
|
218
|
+
expect(err.join("\n")).toContain("ECONNREFUSED");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("search --json refusals are a document with error, exit 1 (#4367)", () => {
|
|
223
|
+
test("a query the grammar refuses carries chant's hint", async () => {
|
|
224
|
+
const { exit, doc } = await search({ path: "effectiveIngress:tcp:22" });
|
|
225
|
+
expect(exit).toBe(1);
|
|
226
|
+
expect(doc).toEqual({
|
|
227
|
+
...HEAD,
|
|
228
|
+
query: "effectiveIngress:tcp:22",
|
|
229
|
+
error: {
|
|
230
|
+
code: "query-invalid",
|
|
231
|
+
message: "\"effectiveIngress:tcp:22\" is not a term — there is no \"effectiveIngress:\" prefix",
|
|
232
|
+
hint: "for an attribute, say attr:effectiveIngress=tcp:22; the prefixes are kind:, attr:, tag:, and ->/<- for edges",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
expect(err.join("\n")).toContain("is not a term");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("--live without --env", async () => {
|
|
239
|
+
const { exit, doc } = await search({ path: "kind:EC2::Instance", live: true });
|
|
240
|
+
expect(exit).toBe(1);
|
|
241
|
+
expect(doc).toEqual({ ...HEAD, query: "kind:EC2::Instance", error: { code: "usage-invalid", message: "chant search --live needs an environment: --live --env <name>" } });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("each refusal carries its code: grouping, flags, environment, snapshot (#4374)", async () => {
|
|
245
|
+
expect((await search({ path: "", countBy: "colour" })).doc.error.code).toBe("query-invalid");
|
|
246
|
+
expect((await search({ path: "kind:EC2::Instance", checkSnapshot: true })).doc.error.code).toBe("usage-invalid");
|
|
247
|
+
expect((await search({ path: "kind:EC2::Instance", snapshot: "latest", at: "other", env: "dev" })).doc.error.code).toBe("usage-invalid");
|
|
248
|
+
loadChantConfigMock.mockResolvedValue({ config: { environments: ["dev"] } });
|
|
249
|
+
expect((await search({ path: "kind:EC2::Instance", live: true, env: "prod" })).doc.error.code).toBe("environment-unknown");
|
|
250
|
+
loadChantConfigMock.mockResolvedValue({ config: {} });
|
|
251
|
+
replaySnapshotsMock.mockResolvedValue({ error: "No snapshots found for environment \"dev\"", hint: "Record one first: chant lifecycle snapshot dev" });
|
|
252
|
+
const snap = await search({ path: "kind:EC2::Instance", snapshot: "latest", env: "dev" });
|
|
253
|
+
expect(snap.exit).toBe(1);
|
|
254
|
+
expect(snap.doc.error).toEqual({ code: "snapshot-unavailable", message: "No snapshots found for environment \"dev\"", hint: "Record one first: chant lifecycle snapshot dev" });
|
|
255
|
+
buildResultMock.mockReturnValue({ errors: [new Error("bad")], entities, outputs: new Map(), warnings: [] });
|
|
256
|
+
expect((await search({ path: "kind:EC2::Instance", live: true, env: "dev" })).doc.error.code).toBe("build-failed");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("without --json a refusal prints nothing on stdout, as before", async () => {
|
|
260
|
+
const exit = await runSearch({ args: makeArgs({ path: "->", json: undefined }), plugins: [], serializers: [] });
|
|
261
|
+
expect(exit).toBe(1);
|
|
262
|
+
expect(out).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("search --json with --check-live (#4367)", () => {
|
|
267
|
+
const snapshot = {
|
|
268
|
+
observations: [{
|
|
269
|
+
lexicon: "aws",
|
|
270
|
+
resources: {
|
|
271
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK", attributes: { SubnetId: "subnet-old" } },
|
|
272
|
+
privateServer: { type: "AWS::EC2::Instance", physicalId: "i-2", status: "OK", attributes: {} },
|
|
273
|
+
},
|
|
274
|
+
}],
|
|
275
|
+
commit: "a1b2c3d4e5f6", timestamp: "2026-08-01T03:15:00.000Z", depth: "identity",
|
|
276
|
+
};
|
|
277
|
+
const liveNow = createMockPlugin({
|
|
278
|
+
name: "aws",
|
|
279
|
+
describeResources: async () => ({
|
|
280
|
+
webServer: { type: "AWS::EC2::Instance", physicalId: "i-1", status: "OK", attributes: { SubnetId: "subnet-new" } },
|
|
281
|
+
privateServer: { type: "AWS::EC2::Instance", physicalId: "i-2", status: "OK", attributes: {} },
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("drift is a field, scoped to the matched rows", async () => {
|
|
286
|
+
replaySnapshotsMock.mockResolvedValue(snapshot);
|
|
287
|
+
loadPluginsMock.mockResolvedValue([liveNow]);
|
|
288
|
+
const { exit, doc } = await search({ path: "kind:EC2::Instance", at: "latest", env: "dev", checkLive: true });
|
|
289
|
+
expect(exit).toBe(0);
|
|
290
|
+
expect(doc.drift).toMatchObject({
|
|
291
|
+
against: "live",
|
|
292
|
+
checked: 2,
|
|
293
|
+
count: 1,
|
|
294
|
+
missing: [],
|
|
295
|
+
orphan: [],
|
|
296
|
+
disappeared: [],
|
|
297
|
+
drifted: [{ id: "webServer", path: "attributes.SubnetId", old: "subnet-old", new: "subnet-new" }],
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("--fail-on-drift exits 1 with the document printed", async () => {
|
|
302
|
+
replaySnapshotsMock.mockResolvedValue(snapshot);
|
|
303
|
+
loadPluginsMock.mockResolvedValue([liveNow]);
|
|
304
|
+
const { exit, doc } = await search({ path: "kind:EC2::Instance", at: "latest", env: "dev", checkLive: true, failOnDrift: true });
|
|
305
|
+
expect(exit).toBe(1);
|
|
306
|
+
expect(doc.drift.count).toBe(1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("--check-snapshot with no snapshot says so in drift.unavailable", async () => {
|
|
310
|
+
replaySnapshotsMock.mockResolvedValue({ error: "no snapshot recorded for dev" });
|
|
311
|
+
loadPluginsMock.mockResolvedValue([liveNow]);
|
|
312
|
+
const { exit, doc } = await search({ path: "kind:EC2::Instance", live: true, env: "dev", checkSnapshot: true });
|
|
313
|
+
expect(exit).toBe(0);
|
|
314
|
+
expect(doc.drift).toEqual({ against: "snapshot", unavailable: "no snapshot recorded for dev" });
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("a query that opens with -> reaches the search (#4369)", () => {
|
|
319
|
+
test("chant search \"->kind:S3::Bucket\" --json searches for that, not for \".\"", async () => {
|
|
320
|
+
const entitiesWithRef = new Map<string, Declarable>([
|
|
321
|
+
["web", instance({ Bucket: { $ref: "bucket" } })],
|
|
322
|
+
["bucket", decl({ lexicon: "aws", entityType: "AWS::S3::Bucket", props: {} })],
|
|
323
|
+
]);
|
|
324
|
+
discoverMock.mockResolvedValue({ entities: entitiesWithRef, errors: [], sourceFiles: [] });
|
|
325
|
+
for (const argv of [["search", "->kind:S3::Bucket", "--json"], ["search", "->kind:S3::Bucket", "--explain", "--json"]]) {
|
|
326
|
+
out = [];
|
|
327
|
+
const exit = await runSearch({ args: parseArgs(argv), plugins: [], serializers: [] });
|
|
328
|
+
expect(exit).toBe(0);
|
|
329
|
+
const doc = JSON.parse(out[0]);
|
|
330
|
+
expect(doc.query).toBe("->kind:S3::Bucket");
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|