@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.
Files changed (190) hide show
  1. package/dist/capabilities/aws.paths +148 -0
  2. package/dist/capabilities/base-mappings.json +122 -0
  3. package/dist/capabilities/catalog.d.ts +89 -0
  4. package/dist/capabilities/catalog.d.ts.map +1 -0
  5. package/dist/capabilities/catalog.js +204 -0
  6. package/dist/capabilities/language.d.ts +103 -0
  7. package/dist/capabilities/language.d.ts.map +1 -0
  8. package/dist/capabilities/language.js +335 -0
  9. package/dist/capabilities/matcher.d.ts +65 -0
  10. package/dist/capabilities/matcher.d.ts.map +1 -0
  11. package/dist/capabilities/matcher.js +299 -0
  12. package/dist/capabilities/view.d.ts +207 -0
  13. package/dist/capabilities/view.d.ts.map +1 -0
  14. package/dist/capabilities/view.js +484 -0
  15. package/dist/cli/commands/lint.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.js +1 -0
  17. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  18. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  19. package/dist/cli/handlers/declared-graph.js +87 -0
  20. package/dist/cli/handlers/graph.d.ts +25 -0
  21. package/dist/cli/handlers/graph.d.ts.map +1 -1
  22. package/dist/cli/handlers/graph.js +93 -87
  23. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  24. package/dist/cli/handlers/pr-env.js +2 -1
  25. package/dist/cli/handlers/search-count.d.ts +89 -0
  26. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  27. package/dist/cli/handlers/search-count.js +132 -0
  28. package/dist/cli/handlers/search.d.ts +358 -6
  29. package/dist/cli/handlers/search.d.ts.map +1 -1
  30. package/dist/cli/handlers/search.js +1065 -155
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/main.js +23 -3
  33. package/dist/cli/mcp/server.d.ts.map +1 -1
  34. package/dist/cli/mcp/server.js +3 -0
  35. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  36. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  37. package/dist/cli/mcp/tools/capabilities.js +27 -0
  38. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  39. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  40. package/dist/cli/mcp/workspace-tools.js +47 -2
  41. package/dist/cli/parse-args.d.ts.map +1 -1
  42. package/dist/cli/parse-args.js +64 -17
  43. package/dist/cli/registry.d.ts +22 -0
  44. package/dist/cli/registry.d.ts.map +1 -1
  45. package/dist/components/pr-env-ports.d.ts +29 -0
  46. package/dist/components/pr-env-ports.d.ts.map +1 -0
  47. package/dist/components/pr-env-ports.js +77 -0
  48. package/dist/components/pr-env-run.d.ts +5 -21
  49. package/dist/components/pr-env-run.d.ts.map +1 -1
  50. package/dist/components/pr-env-run.js +6 -70
  51. package/dist/components/pr-environment.d.ts +2 -1
  52. package/dist/components/pr-environment.d.ts.map +1 -1
  53. package/dist/components/pr-environment.js +1 -1
  54. package/dist/config-environments.d.ts +92 -0
  55. package/dist/config-environments.d.ts.map +1 -0
  56. package/dist/config-environments.js +102 -0
  57. package/dist/config.d.ts +24 -84
  58. package/dist/config.d.ts.map +1 -1
  59. package/dist/config.js +9 -96
  60. package/dist/graph-detail.d.ts.map +1 -1
  61. package/dist/graph-detail.js +15 -3
  62. package/dist/graph-effective.d.ts +33 -2
  63. package/dist/graph-effective.d.ts.map +1 -1
  64. package/dist/graph-effective.js +215 -68
  65. package/dist/graph-ir.d.ts +23 -0
  66. package/dist/graph-ir.d.ts.map +1 -1
  67. package/dist/graph-ir.js +130 -0
  68. package/dist/graph-sub-template.d.ts +11 -0
  69. package/dist/graph-sub-template.d.ts.map +1 -0
  70. package/dist/graph-sub-template.js +19 -0
  71. package/dist/lexicon.d.ts +9 -0
  72. package/dist/lexicon.d.ts.map +1 -1
  73. package/dist/lifecycle/observe.d.ts +28 -0
  74. package/dist/lifecycle/observe.d.ts.map +1 -1
  75. package/dist/lifecycle/observe.js +23 -1
  76. package/dist/lint/rule.d.ts +6 -0
  77. package/dist/lint/rule.d.ts.map +1 -1
  78. package/dist/live-endpoint.d.ts +1 -1
  79. package/dist/live-endpoint.d.ts.map +1 -1
  80. package/dist/live-endpoint.js +1 -1
  81. package/dist/observation.d.ts +48 -1
  82. package/dist/observation.d.ts.map +1 -1
  83. package/dist/observation.js +21 -5
  84. package/dist/op/activities/activity-contracts.d.ts +4 -4
  85. package/dist/wave-runtime.mjs +250 -249
  86. package/dist/workspace/capabilities-contract.d.ts +66 -0
  87. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  88. package/dist/workspace/capabilities-contract.js +107 -0
  89. package/dist/workspace/capabilities.schema.json +229 -0
  90. package/dist/workspace/compose-graph.d.ts +9 -0
  91. package/dist/workspace/compose-graph.d.ts.map +1 -1
  92. package/dist/workspace/compose-graph.js +3 -1
  93. package/dist/workspace/conformance/index.d.ts +1 -1
  94. package/dist/workspace/conformance/index.d.ts.map +1 -1
  95. package/dist/workspace/conformance/index.js +39 -2
  96. package/dist/workspace/graph-cache.d.ts +8 -0
  97. package/dist/workspace/graph-cache.d.ts.map +1 -1
  98. package/dist/workspace/graph-cache.js +27 -3
  99. package/dist/workspace/graph-cli.d.ts +39 -2
  100. package/dist/workspace/graph-cli.d.ts.map +1 -1
  101. package/dist/workspace/graph-cli.js +66 -24
  102. package/dist/workspace/graph.schema.json +148 -0
  103. package/dist/workspace/kind-readers.d.ts +3 -0
  104. package/dist/workspace/kind-readers.d.ts.map +1 -1
  105. package/dist/workspace/kind-readers.js +7 -1
  106. package/dist/workspace/member-commands.d.ts +10 -1
  107. package/dist/workspace/member-commands.d.ts.map +1 -1
  108. package/dist/workspace/member-commands.js +17 -1
  109. package/dist/workspace/nested-graph.d.ts +2 -0
  110. package/dist/workspace/nested-graph.d.ts.map +1 -1
  111. package/dist/workspace/nested-graph.js +32 -1
  112. package/dist/workspace/reason-codes.d.ts +10 -0
  113. package/dist/workspace/reason-codes.d.ts.map +1 -1
  114. package/dist/workspace/reason-codes.js +12 -0
  115. package/dist/workspace/search-cli.d.ts +204 -0
  116. package/dist/workspace/search-cli.d.ts.map +1 -0
  117. package/dist/workspace/search-cli.js +466 -0
  118. package/dist/workspace/search-contract.d.ts +56 -0
  119. package/dist/workspace/search-contract.d.ts.map +1 -0
  120. package/dist/workspace/search-contract.js +70 -0
  121. package/dist/workspace/search.schema.json +822 -0
  122. package/package.json +1 -1
  123. package/src/__snapshots__/okf.test.ts.snap +10 -0
  124. package/src/capabilities/aws.paths +148 -0
  125. package/src/capabilities/base-mappings.json +122 -0
  126. package/src/capabilities/catalog.ts +230 -0
  127. package/src/capabilities/language.test.ts +191 -0
  128. package/src/capabilities/language.ts +373 -0
  129. package/src/capabilities/matcher.ts +315 -0
  130. package/src/capabilities/view-compose.test.ts +76 -0
  131. package/src/capabilities/view.ts +631 -0
  132. package/src/cli/commands/lint.ts +1 -0
  133. package/src/cli/handlers/declared-graph.ts +99 -0
  134. package/src/cli/handlers/graph.test.ts +94 -3
  135. package/src/cli/handlers/graph.ts +123 -96
  136. package/src/cli/handlers/pr-env.ts +1 -2
  137. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  138. package/src/cli/handlers/search-count.test.ts +191 -0
  139. package/src/cli/handlers/search-count.ts +165 -0
  140. package/src/cli/handlers/search-json.test.ts +333 -0
  141. package/src/cli/handlers/search-live.test.ts +387 -0
  142. package/src/cli/handlers/search-paths.test.ts +235 -0
  143. package/src/cli/handlers/search.ts +1312 -166
  144. package/src/cli/main.test.ts +28 -3
  145. package/src/cli/main.ts +23 -3
  146. package/src/cli/mcp/server.test.ts +3 -3
  147. package/src/cli/mcp/server.ts +3 -0
  148. package/src/cli/mcp/tools/capabilities.ts +32 -0
  149. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  150. package/src/cli/mcp/workspace-tools.ts +47 -2
  151. package/src/cli/parse-args.ts +52 -13
  152. package/src/cli/registry.ts +22 -0
  153. package/src/components/pr-env-ports.ts +89 -0
  154. package/src/components/pr-env-run-bundle.test.ts +54 -0
  155. package/src/components/pr-env-run.ts +5 -76
  156. package/src/components/pr-environment.ts +2 -1
  157. package/src/config-environments.ts +133 -0
  158. package/src/config.ts +37 -124
  159. package/src/graph-detail.ts +15 -3
  160. package/src/graph-effective.ts +226 -75
  161. package/src/graph-ir.test.ts +163 -0
  162. package/src/graph-ir.ts +147 -0
  163. package/src/graph-sub-template.ts +22 -0
  164. package/src/lexicon.ts +10 -0
  165. package/src/lifecycle/observe.test.ts +27 -1
  166. package/src/lifecycle/observe.ts +43 -1
  167. package/src/lint/rule.ts +6 -0
  168. package/src/live-endpoint.ts +1 -1
  169. package/src/observation.test.ts +25 -0
  170. package/src/observation.ts +69 -5
  171. package/src/workspace/capabilities-contract.test.ts +88 -0
  172. package/src/workspace/capabilities-contract.ts +132 -0
  173. package/src/workspace/capabilities.schema.json +229 -0
  174. package/src/workspace/compose-graph.ts +10 -1
  175. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  176. package/src/workspace/conformance/index.ts +31 -2
  177. package/src/workspace/graph-cache.ts +27 -3
  178. package/src/workspace/graph-cli.ts +96 -24
  179. package/src/workspace/graph.schema.json +148 -0
  180. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  181. package/src/workspace/kind-readers.test.ts +1 -1
  182. package/src/workspace/kind-readers.ts +7 -1
  183. package/src/workspace/member-commands.ts +22 -1
  184. package/src/workspace/nested-graph.ts +32 -1
  185. package/src/workspace/reason-codes.test.ts +7 -0
  186. package/src/workspace/reason-codes.ts +12 -0
  187. package/src/workspace/search-cli.e2e.test.ts +305 -0
  188. package/src/workspace/search-cli.ts +591 -0
  189. package/src/workspace/search-contract.ts +95 -0
  190. package/src/workspace/search.schema.json +822 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * `chant workspace search` (#4374): `chant search` in every member, under the
3
+ * member's own chant, composed into one `search` document of the read
4
+ * contract with the ids `chant workspace graph` gives the same nodes.
5
+ *
6
+ * The workspace here has two chant members, `east` and `west`, each a copy of
7
+ * the reader conformance fixture's `delivery` member, and both run this
8
+ * checkout's real chant. A member that runs a fake chant shows the reason
9
+ * codes for a chant too old to print the document and for a read that fails.
10
+ */
11
+
12
+ import { cpSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
13
+ import { join, resolve } from "node:path";
14
+ import { pathToFileURL } from "node:url";
15
+ import { realpathSync } from "node:fs";
16
+ import { afterAll, describe, expect, test } from "vitest";
17
+ import { writeSnapshot } from "../lifecycle/git";
18
+ import { cleanScratch, commitAll, contract, FAKE_GRAPH_CHANT, git, REPO, scratchDir } from "./__fixtures__/contract-repo";
19
+ import { CONFORMANCE_FIXTURE_DIR } from "./conformance";
20
+ import { workspaceGraph } from "./graph-cli";
21
+ import { readerBin, type Toolchain } from "./member-commands";
22
+ import { SEARCH_DOCUMENT_ERROR_CODES, SEARCH_MEMBER_REASON_CODES, SEARCH_OUTPUT_SCHEMA_ID } from "./search-contract";
23
+ import { workspaceSearch, type WorkspaceSearchDocument } from "./search-cli";
24
+ import schema from "./search.schema.json";
25
+
26
+ afterAll(cleanScratch);
27
+ // workspace graph's member cache, kept out of the user's cache directory.
28
+ process.env.CHANT_CACHE_DIR = scratchDir("chant-ws-search-cache-");
29
+
30
+ const TIMEOUT = 240_000;
31
+ const { expectValid } = contract(schema);
32
+
33
+ /** This checkout's chant, started the way the CLI starts it. */
34
+ const reader: Toolchain = {
35
+ command: [process.execPath, "--import", pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href, join(REPO, "packages", "core", "src", "cli", "main.ts")],
36
+ identity: realpathSync(readerBin()),
37
+ source: "reader",
38
+ };
39
+
40
+ /** A git repository with `east` and `west`, two chant members, and whatever else `extra` declares, committed. */
41
+ function twoMembers(extra: { name: string; dir: string; kind: string }[] = []): string {
42
+ const root = scratchDir("chant-ws-search-");
43
+ for (const m of ["east", "west"]) cpSync(join(CONFORMANCE_FIXTURE_DIR, "delivery"), join(root, m), { recursive: true });
44
+ mkdirSync(join(root, "node_modules", "@intentius"), { recursive: true });
45
+ symlinkSync(resolve(REPO, "packages", "core"), join(root, "node_modules", "@intentius", "chant"), "dir");
46
+ writeFileSync(join(root, ".gitignore"), "node_modules/\n");
47
+ writeFileSync(
48
+ join(root, "chant.workspace.json"),
49
+ JSON.stringify({ name: "two", schema: 1, members: [{ name: "east", dir: "east", kind: "chant" }, { name: "west", dir: "west", kind: "chant" }, ...extra] }, null, 2),
50
+ );
51
+ git(root, "init", "-q");
52
+ commitAll(root, "two members");
53
+ return root;
54
+ }
55
+
56
+ function result(doc: WorkspaceSearchDocument): Extract<WorkspaceSearchDocument, { matches: unknown }> {
57
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
58
+ if (!("matches" in doc)) throw new Error("expected rows, got groups");
59
+ return doc;
60
+ }
61
+
62
+ const search = (cwd: string, query: string, extra: Partial<Parameters<typeof workspaceSearch>[0]> = {}) => workspaceSearch({ cwd, query, reader, ...extra });
63
+
64
+ describe("the search schema (#4374)", () => {
65
+ test("lists exactly the codes the code can return", () => {
66
+ expect(schema.$id).toBe(SEARCH_OUTPUT_SCHEMA_ID);
67
+ expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual(SEARCH_DOCUMENT_ERROR_CODES);
68
+ expect(schema.$defs.member.properties.reason.oneOf[1].properties!.code.enum).toEqual([...SEARCH_MEMBER_REASON_CODES]);
69
+ });
70
+ });
71
+
72
+ describe("chant workspace search on two members", () => {
73
+ test("ids are the <member>/<id> ids chant workspace graph prints for the same nodes", async () => {
74
+ const root = twoMembers();
75
+ const [found, graph] = await Promise.all([search(root, "kind:Fixture::Service"), workspaceGraph({ cwd: root, reader })]);
76
+ const doc = result(found.doc);
77
+ expectValid(doc);
78
+ expect(found.failed).toBe(false);
79
+ if ("error" in graph.doc) throw new Error(graph.doc.error.message);
80
+ const graphIds = new Set(graph.doc.nodes.filter((n) => n.kind === "Fixture::Service").map((n) => n.id));
81
+ expect(doc.matches.map((m) => m.id)).toEqual([...graphIds].sort());
82
+ expect(doc.matches.map((m) => m.id)).toEqual(["east/appService", "west/appService"]);
83
+ expect(doc.matches.map((m) => m.member)).toEqual(["east", "west"]);
84
+ // Grouped by member, each searched alone, stamped by the graph cache's rule.
85
+ expect(doc.members.map((m) => [m.name, m.status, m.alone, m.matches])).toEqual([
86
+ ["east", "searched", true, ["east/appService"]],
87
+ ["west", "searched", true, ["west/appService"]],
88
+ ]);
89
+ const graphStamps = Object.fromEntries(graph.doc.members.map((m) => [m.name, m.stamp]));
90
+ for (const m of doc.members) expect(m.stamp, m.name).toBe(graphStamps[m.name]);
91
+ expect(doc).toMatchObject({ contract: 1, workspace: { name: "two", root: "." }, at: null, source: { kind: "declared" } });
92
+ }, TIMEOUT);
93
+
94
+ test("--count-by kind adds the members' groups up, and validates", async () => {
95
+ const root = twoMembers();
96
+ const { doc } = await search(root, "", { flags: { countBy: "kind" } });
97
+ expectValid(doc);
98
+ expect(doc).toMatchObject({ countBy: "kind", total: 2 });
99
+ if (!("groups" in doc)) throw new Error("no groups");
100
+ const service = doc.groups.find((g) => g.group === "Fixture::Service");
101
+ expect(service).toMatchObject({ count: 2 });
102
+ // #4382: each group's ids, as `<member>/<id>`, one per member here.
103
+ expect(service?.ids).toHaveLength(2);
104
+ expect(service?.ids?.map((id) => id.split("/")[0]).sort()).toEqual(doc.members.map((m) => m.name).sort());
105
+ expect(doc.members.map((m) => m.total)).toEqual([1, 1]);
106
+ }, TIMEOUT);
107
+
108
+ test("a refused query is a failure document with a code, before any member runs", async () => {
109
+ const root = twoMembers();
110
+ const { doc, failed } = await search(root, "effectiveIngress:tcp:22");
111
+ expectValid(doc);
112
+ expect(failed).toBe(true);
113
+ expect(doc).toMatchObject({ error: { code: "query-invalid" } });
114
+ }, TIMEOUT);
115
+ });
116
+
117
+ describe("two times: --at is the git revision, --snapshot the recorded estate", () => {
118
+ test("--at <rev> searches the source at that commit; --snapshot latest replays the snapshot; the two are not confused", async () => {
119
+ const root = twoMembers();
120
+ const head = git(root, "rev-parse", "HEAD");
121
+ // The working tree renames east's app; the commit still has the old name.
122
+ const app = join(root, "east", "src", "app.ts");
123
+ writeFileSync(app, readFileSync(app, "utf-8").replace("export const app =", "export const site ="));
124
+
125
+ const now = result((await search(root, "kind:Fixture::Service", { members: ["east"] })).doc);
126
+ expect(now.matches.map((m) => m.id)).toEqual(["east/siteService"]);
127
+ expect(now.at).toBeNull();
128
+
129
+ const then = result((await search(root, "kind:Fixture::Service", { members: ["east"], at: head })).doc);
130
+ expectValid(then);
131
+ expect(then.at).toBe(head);
132
+ expect(then.source).toEqual({ kind: "declared" });
133
+ expect(then.matches.map((m) => m.id)).toEqual(["east/appService"]);
134
+
135
+ // A snapshot recorded for east only: east replays it, west has none and says so.
136
+ await writeSnapshot(
137
+ "dev",
138
+ "fixture",
139
+ JSON.stringify({ lexicon: "fixture", environment: "dev", commit: "c0ffee", timestamp: "2026-10-23T00:00:00.000Z", resources: { siteService: { type: "Fixture::Service", physicalId: "svc-east", status: "OK" } } }),
140
+ { cwd: join(root, "east") },
141
+ );
142
+ const replayed = await search(root, "kind:Fixture::Service", { flags: { snapshot: "latest", env: "dev" } });
143
+ const snap = result(replayed.doc);
144
+ expectValid(snap);
145
+ expect(snap.at).toBeNull();
146
+ expect(snap.source).toEqual({ kind: "snapshot", env: "dev", snapshot: "latest" });
147
+ expect(snap.matches).toEqual([expect.objectContaining({ id: "east/siteService", physicalId: "svc-east" })]);
148
+ expect(snap.members[0]).toMatchObject({ name: "east", status: "searched", source: { kind: "snapshot", snapshot: "latest", commit: "c0ffee" } });
149
+ expect(snap.members[1]).toMatchObject({ name: "west", status: "failed", reason: { code: "snapshot-unavailable" } });
150
+ expect(replayed.failed).toBe(true);
151
+
152
+ // --at given a snapshot ref names no commit, and the hint names --snapshot.
153
+ const misused = await search(root, "kind:Fixture::Service", { at: "latest" });
154
+ expectValid(misused.doc);
155
+ expect(misused.doc).toMatchObject({ error: { code: "revision-unknown", hint: expect.stringContaining("--snapshot latest") } });
156
+ // Both at once is refused.
157
+ const both = await search(root, "kind:Fixture::Service", { at: head, flags: { snapshot: "latest", env: "dev" } });
158
+ expectValid(both.doc);
159
+ expect(both.doc).toMatchObject({ error: { code: "snapshot-at-revision" } });
160
+ }, TIMEOUT);
161
+ });
162
+
163
+ describe("a member that can't be searched has a reason code, never zero matches", () => {
164
+ test("a member whose chant is too old: member-chant-too-old", async () => {
165
+ const root = twoMembers([{ name: "old", dir: "old", kind: "chant" }]);
166
+ mkdirSync(join(root, "old", "node_modules", ".bin"), { recursive: true });
167
+ writeFileSync(join(root, "old", "chant.config.ts"), "export default {};\n");
168
+ writeFileSync(join(root, "old", "ids.txt"), "Thing\n");
169
+ writeFileSync(join(root, "old", "node_modules", ".bin", "chant"), FAKE_GRAPH_CHANT, { mode: 0o755 });
170
+ const { doc, failed } = await search(root, "kind:Fixture::Service");
171
+ expectValid(doc);
172
+ expect(failed).toBe(true);
173
+ const r = result(doc);
174
+ expect(r.members.find((m) => m.name === "old")).toMatchObject({ status: "failed", chant: null, reason: { code: "member-chant-too-old" } });
175
+ expect(r.members.find((m) => m.name === "old")).not.toHaveProperty("matches");
176
+ expect(r.matches.map((m) => m.id)).toEqual(["east/appService", "west/appService"]);
177
+ }, TIMEOUT);
178
+
179
+ test("a member whose read fails: command-failed, and a missing directory: dir-missing", async () => {
180
+ const root = twoMembers([{ name: "gone", dir: "gone", kind: "chant" }]);
181
+ writeFileSync(join(root, "west", "chant.config.ts"), 'throw new Error("this config does not load");\n');
182
+ const { doc, failed } = await search(root, "kind:Fixture::Service");
183
+ expectValid(doc);
184
+ expect(failed).toBe(true);
185
+ const r = result(doc);
186
+ expect(r.members.find((m) => m.name === "west")).toMatchObject({ status: "failed", reason: { code: "command-failed" } });
187
+ expect(r.members.find((m) => m.name === "gone")).toMatchObject({ status: "failed", reason: { code: "dir-missing" } });
188
+ expect(r.matches.map((m) => m.id)).toEqual(["east/appService"]);
189
+ }, TIMEOUT);
190
+ });
191
+
192
+ // #4380: a member of kind terraform is searched through the reader project
193
+ // `workspace graph` writes for it (#2874), so its rows carry the ids the graph
194
+ // gives the same nodes: `<member>/<root>/<address>`, the root named for the member.
195
+ describe("terraform members (#4380)", () => {
196
+ const LEXICON = "@intentius/chant-lexicon-terraform";
197
+ const VERSION = (JSON.parse(readFileSync(join(REPO, "lexicons", "terraform", "package.json"), "utf-8")) as { version: string }).version;
198
+ const NET_TF = 'resource "aws_vpc" "main" {\n cidr_block = "10.0.0.0/16"\n}\n\nresource "aws_subnet" "b" {\n vpc_id = aws_vpc.main.id\n cidr_block = "10.0.2.0/24"\n}\n\nresource "aws_s3_bucket" "logs" {\n bucket = "logs"\n}\n\nmodule "subnets" {\n source = "../modules/subnets"\n vpc_id = aws_vpc.main.id\n}\n';
199
+ const SUBNETS_TF = 'variable "vpc_id" {}\n\nresource "aws_subnet" "a" {\n vpc_id = var.vpc_id\n cidr_block = "10.0.1.0/24"\n}\n';
200
+
201
+ /** A committed workspace with chant member `east` and terraform member `net`, the lexicon pinned at `version`. */
202
+ function mixed(version = VERSION): string {
203
+ const root = scratchDir("chant-ws-search-tf-");
204
+ cpSync(join(CONFORMANCE_FIXTURE_DIR, "delivery"), join(root, "east"), { recursive: true });
205
+ mkdirSync(join(root, "estates", "net"), { recursive: true });
206
+ mkdirSync(join(root, "estates", "modules", "subnets"), { recursive: true });
207
+ writeFileSync(join(root, "estates", "net", "main.tf"), NET_TF);
208
+ writeFileSync(join(root, "estates", "modules", "subnets", "main.tf"), SUBNETS_TF);
209
+ // This checkout's node_modules: @intentius/chant for east, the lexicon for net's reader project.
210
+ symlinkSync(join(REPO, "node_modules"), join(root, "node_modules"), "dir");
211
+ writeFileSync(join(root, ".gitignore"), "node_modules\n");
212
+ writeFileSync(
213
+ join(root, "chant.workspace.json"),
214
+ JSON.stringify({ name: "mixed", schema: 1, pins: [{ package: LEXICON, version }], members: [{ name: "east", dir: "east", kind: "chant" }, { name: "net", dir: "estates/net", kind: "terraform" }] }, null, 2),
215
+ );
216
+ git(root, "init", "-q");
217
+ commitAll(root, "mixed");
218
+ return root;
219
+ }
220
+
221
+ test("a terraform and a chant member: rows, hops and capability matches carry workspace graph's ids", async () => {
222
+ const root = mixed();
223
+ const [found, graph] = await Promise.all([search(root, "kind:aws"), workspaceGraph({ cwd: root, reader })]);
224
+ const doc = result(found.doc);
225
+ expectValid(doc);
226
+ expect(found.failed).toBe(false);
227
+ if ("error" in graph.doc) throw new Error(graph.doc.error.message);
228
+ expect(doc.members.map((m) => [m.name, m.kind, m.status, m.reason])).toEqual([
229
+ ["east", "chant", "searched", null],
230
+ ["net", "terraform", "searched", null],
231
+ ]);
232
+ expect(doc.matches.map((m) => m.id)).toEqual(["net/net/aws_s3_bucket.logs", "net/net/aws_subnet.b", "net/net/aws_vpc.main", "net/net/module.subnets/aws_subnet.a"]);
233
+ expect(doc.members.find((m) => m.name === "east")!.matches).toEqual([]);
234
+ const graphIds = new Set(graph.doc.nodes.map((n) => n.id));
235
+ for (const m of doc.matches) expect(graphIds, m.id).toContain(m.id);
236
+ // The member's stamp is the one workspace graph gives it.
237
+ expect(doc.members.find((m) => m.name === "net")!.stamp).toBe(graph.doc.members.find((m) => m.name === "net")!.stamp);
238
+
239
+ // A path's hops are prefixed too, and each end is a node of the graph.
240
+ const walked = result((await search(root, "kind:aws_subnet ->kind:aws_vpc")).doc);
241
+ expectValid(walked);
242
+ expect(walked.matches.map((m) => m.id)).toEqual(["net/net/aws_subnet.b"]);
243
+ const hops = walked.matches[0].paths.flatMap((p) => p.hops);
244
+ expect(hops.length).toBeGreaterThan(0);
245
+ for (const h of hops) {
246
+ expect(graphIds, h.from).toContain(h.from);
247
+ expect(graphIds, h.to).toContain(h.to);
248
+ }
249
+ expect(hops.at(-1)!.to).toBe("net/net/aws_vpc.main");
250
+
251
+ // A capability term (#4375) maps the terraform member's nodes as well.
252
+ const caps = result((await search(root, "cap:stores-data")).doc);
253
+ expect(caps.matches.map((m) => m.id)).toEqual(["net/net/aws_s3_bucket.logs"]);
254
+ expect(caps.members.find((m) => m.name === "net")).toMatchObject({ status: "searched", matches: ["net/net/aws_s3_bucket.logs"] });
255
+ }, TIMEOUT);
256
+
257
+ test("a pin at another version than the installed lexicon: unknown-kind naming the pin, in search and graph, never kind-not-run", async () => {
258
+ const root = mixed("0.0.1");
259
+ const [found, graph] = await Promise.all([search(root, "kind:aws_vpc"), workspaceGraph({ cwd: root, reader })]);
260
+ const doc = result(found.doc);
261
+ expectValid(doc);
262
+ expect(found.failed).toBe(true);
263
+ const net = doc.members.find((m) => m.name === "net")!;
264
+ expect(net).toMatchObject({ status: "failed", reason: { code: "unknown-kind" } });
265
+ expect(net.reason!.message).toContain(`${LEXICON} is pinned at 0.0.1, and ${VERSION} is installed`);
266
+ expect(net).not.toHaveProperty("matches");
267
+ if ("error" in graph.doc) throw new Error(graph.doc.error.message);
268
+ expect(graph.doc.members.find((m) => m.name === "net")).toMatchObject({ status: "failed", reason: { code: "unknown-kind" } });
269
+ expect(graph.failed).toBe(true);
270
+ }, TIMEOUT);
271
+
272
+ // The shape behold's scratch project takes for a bare Terraform folder
273
+ // (behold#925): a directory outside the folder, not a git repository, with
274
+ // the declaration, a node_modules link, and a link to the folder as the one
275
+ // member's directory. Nothing is written in the folder.
276
+ test("a one-member workspace whose member is a bare Terraform folder, linked in: ids equal workspace graph's, the folder untouched", async () => {
277
+ const estate = scratchDir("chant-ws-search-bare-");
278
+ writeFileSync(join(estate, "main.tf"), 'resource "aws_vpc" "main" {\n cidr_block = "10.0.0.0/16"\n}\n\nresource "aws_s3_bucket" "logs" {\n bucket = "logs"\n}\n\nmodule "subnets" {\n source = "./modules/subnets"\n vpc_id = aws_vpc.main.id\n}\n');
279
+ mkdirSync(join(estate, "modules", "subnets"), { recursive: true });
280
+ writeFileSync(join(estate, "modules", "subnets", "main.tf"), SUBNETS_TF);
281
+ const before = readdirSync(estate, { recursive: true }).sort();
282
+
283
+ const project = scratchDir("chant-ws-search-scratch-");
284
+ symlinkSync(join(REPO, "node_modules"), join(project, "node_modules"), "dir");
285
+ symlinkSync(estate, join(project, "estate"), "dir");
286
+ writeFileSync(
287
+ join(project, "chant.workspace.json"),
288
+ JSON.stringify({ name: "estate", schema: 1, pins: [{ package: LEXICON, version: VERSION }], members: [{ name: "estate", dir: "estate", kind: "terraform" }] }, null, 2),
289
+ );
290
+
291
+ const [found, graph] = await Promise.all([search(project, "kind:aws"), workspaceGraph({ cwd: project, reader })]);
292
+ const doc = result(found.doc);
293
+ expectValid(doc);
294
+ expect(found.failed).toBe(false);
295
+ if ("error" in graph.doc) throw new Error(graph.doc.error.message);
296
+ expect(doc.at).toBeNull();
297
+ expect(doc.members.map((m) => [m.name, m.kind, m.status])).toEqual([["estate", "terraform", "searched"]]);
298
+ expect(doc.matches.map((m) => m.id)).toEqual(["estate/estate/aws_s3_bucket.logs", "estate/estate/aws_vpc.main", "estate/estate/module.subnets/aws_subnet.a"]);
299
+ const graphIds = new Set(graph.doc.nodes.map((n) => n.id));
300
+ for (const m of doc.matches) expect(graphIds, m.id).toContain(m.id);
301
+ const caps = result((await search(project, "cap:stores-data")).doc);
302
+ expect(caps.matches.map((m) => m.id)).toEqual(["estate/estate/aws_s3_bucket.logs"]);
303
+ expect(readdirSync(estate, { recursive: true }).sort()).toEqual(before);
304
+ }, TIMEOUT);
305
+ });