@salesforce/graphiti 10.10.2

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 (282) hide show
  1. package/AGENT_GUIDE.md +424 -0
  2. package/CHANGELOG.md +448 -0
  3. package/LICENSE.txt +82 -0
  4. package/README.md +204 -0
  5. package/TASK.md +249 -0
  6. package/dist/cli.d.ts +7 -0
  7. package/dist/cli.js +683 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/args.d.ts +13 -0
  10. package/dist/commands/args.js +207 -0
  11. package/dist/commands/args.js.map +1 -0
  12. package/dist/commands/build.d.ts +11 -0
  13. package/dist/commands/build.js +209 -0
  14. package/dist/commands/build.js.map +1 -0
  15. package/dist/commands/connect.d.ts +8 -0
  16. package/dist/commands/connect.js +55 -0
  17. package/dist/commands/connect.js.map +1 -0
  18. package/dist/commands/describe.d.ts +6 -0
  19. package/dist/commands/describe.js +229 -0
  20. package/dist/commands/describe.js.map +1 -0
  21. package/dist/commands/meta.d.ts +9 -0
  22. package/dist/commands/meta.js +140 -0
  23. package/dist/commands/meta.js.map +1 -0
  24. package/dist/commands/navigate.d.ts +14 -0
  25. package/dist/commands/navigate.js +105 -0
  26. package/dist/commands/navigate.js.map +1 -0
  27. package/dist/commands/query-helpers.d.ts +80 -0
  28. package/dist/commands/query-helpers.js +865 -0
  29. package/dist/commands/query-helpers.js.map +1 -0
  30. package/dist/commands/query.d.ts +26 -0
  31. package/dist/commands/query.js +901 -0
  32. package/dist/commands/query.js.map +1 -0
  33. package/dist/commands/review.d.ts +18 -0
  34. package/dist/commands/review.js +533 -0
  35. package/dist/commands/review.js.map +1 -0
  36. package/dist/commands/session-mgmt.d.ts +25 -0
  37. package/dist/commands/session-mgmt.js +206 -0
  38. package/dist/commands/session-mgmt.js.map +1 -0
  39. package/dist/commands/type.d.ts +6 -0
  40. package/dist/commands/type.js +342 -0
  41. package/dist/commands/type.js.map +1 -0
  42. package/dist/commands/validate-input.d.ts +6 -0
  43. package/dist/commands/validate-input.js +32 -0
  44. package/dist/commands/validate-input.js.map +1 -0
  45. package/dist/intent/build-aggregate.d.ts +33 -0
  46. package/dist/intent/build-aggregate.js +134 -0
  47. package/dist/intent/build-aggregate.js.map +1 -0
  48. package/dist/intent/build-create.d.ts +14 -0
  49. package/dist/intent/build-create.js +16 -0
  50. package/dist/intent/build-create.js.map +1 -0
  51. package/dist/intent/build-delete.d.ts +30 -0
  52. package/dist/intent/build-delete.js +53 -0
  53. package/dist/intent/build-delete.js.map +1 -0
  54. package/dist/intent/build-detail.d.ts +32 -0
  55. package/dist/intent/build-detail.js +80 -0
  56. package/dist/intent/build-detail.js.map +1 -0
  57. package/dist/intent/build-discover.d.ts +30 -0
  58. package/dist/intent/build-discover.js +149 -0
  59. package/dist/intent/build-discover.js.map +1 -0
  60. package/dist/intent/build-list.d.ts +28 -0
  61. package/dist/intent/build-list.js +75 -0
  62. package/dist/intent/build-list.js.map +1 -0
  63. package/dist/intent/build-mutation.d.ts +23 -0
  64. package/dist/intent/build-mutation.js +54 -0
  65. package/dist/intent/build-mutation.js.map +1 -0
  66. package/dist/intent/build-output.d.ts +27 -0
  67. package/dist/intent/build-output.js +60 -0
  68. package/dist/intent/build-output.js.map +1 -0
  69. package/dist/intent/build-raw.d.ts +23 -0
  70. package/dist/intent/build-raw.js +54 -0
  71. package/dist/intent/build-raw.js.map +1 -0
  72. package/dist/intent/build-update.d.ts +14 -0
  73. package/dist/intent/build-update.js +16 -0
  74. package/dist/intent/build-update.js.map +1 -0
  75. package/dist/intent/get-schema-with-priming.d.ts +26 -0
  76. package/dist/intent/get-schema-with-priming.js +32 -0
  77. package/dist/intent/get-schema-with-priming.js.map +1 -0
  78. package/dist/intent/select-child-relationship.d.ts +19 -0
  79. package/dist/intent/select-child-relationship.js +38 -0
  80. package/dist/intent/select-child-relationship.js.map +1 -0
  81. package/dist/intent/types.d.ts +159 -0
  82. package/dist/intent/types.js +21 -0
  83. package/dist/intent/types.js.map +1 -0
  84. package/dist/lib/apply-command.d.ts +15 -0
  85. package/dist/lib/apply-command.js +238 -0
  86. package/dist/lib/apply-command.js.map +1 -0
  87. package/dist/lib/auth.d.ts +38 -0
  88. package/dist/lib/auth.js +113 -0
  89. package/dist/lib/auth.js.map +1 -0
  90. package/dist/lib/codegen.d.ts +32 -0
  91. package/dist/lib/codegen.js +700 -0
  92. package/dist/lib/codegen.js.map +1 -0
  93. package/dist/lib/command-registry.d.ts +59 -0
  94. package/dist/lib/command-registry.js +366 -0
  95. package/dist/lib/command-registry.js.map +1 -0
  96. package/dist/lib/formatter.d.ts +76 -0
  97. package/dist/lib/formatter.js +419 -0
  98. package/dist/lib/formatter.js.map +1 -0
  99. package/dist/lib/fs-utils.d.ts +23 -0
  100. package/dist/lib/fs-utils.js +46 -0
  101. package/dist/lib/fs-utils.js.map +1 -0
  102. package/dist/lib/graphql-name.d.ts +27 -0
  103. package/dist/lib/graphql-name.js +32 -0
  104. package/dist/lib/graphql-name.js.map +1 -0
  105. package/dist/lib/interactive.d.ts +6 -0
  106. package/dist/lib/interactive.js +562 -0
  107. package/dist/lib/interactive.js.map +1 -0
  108. package/dist/lib/introspect.d.ts +40 -0
  109. package/dist/lib/introspect.js +280 -0
  110. package/dist/lib/introspect.js.map +1 -0
  111. package/dist/lib/object-info.d.ts +79 -0
  112. package/dist/lib/object-info.js +313 -0
  113. package/dist/lib/object-info.js.map +1 -0
  114. package/dist/lib/path-selection.d.ts +50 -0
  115. package/dist/lib/path-selection.js +146 -0
  116. package/dist/lib/path-selection.js.map +1 -0
  117. package/dist/lib/prime-schema.d.ts +59 -0
  118. package/dist/lib/prime-schema.js +158 -0
  119. package/dist/lib/prime-schema.js.map +1 -0
  120. package/dist/lib/query-builder.d.ts +10 -0
  121. package/dist/lib/query-builder.js +168 -0
  122. package/dist/lib/query-builder.js.map +1 -0
  123. package/dist/lib/query-commands.d.ts +19 -0
  124. package/dist/lib/query-commands.js +262 -0
  125. package/dist/lib/query-commands.js.map +1 -0
  126. package/dist/lib/session.d.ts +205 -0
  127. package/dist/lib/session.js +1075 -0
  128. package/dist/lib/session.js.map +1 -0
  129. package/dist/lib/tokenize.d.ts +12 -0
  130. package/dist/lib/tokenize.js +48 -0
  131. package/dist/lib/tokenize.js.map +1 -0
  132. package/dist/lib/uiapi.d.ts +109 -0
  133. package/dist/lib/uiapi.js +159 -0
  134. package/dist/lib/uiapi.js.map +1 -0
  135. package/dist/lib/validator.d.ts +27 -0
  136. package/dist/lib/validator.js +100 -0
  137. package/dist/lib/validator.js.map +1 -0
  138. package/dist/lib/variable-promotion.d.ts +69 -0
  139. package/dist/lib/variable-promotion.js +95 -0
  140. package/dist/lib/variable-promotion.js.map +1 -0
  141. package/dist/lib/walker.d.ts +147 -0
  142. package/dist/lib/walker.js +723 -0
  143. package/dist/lib/walker.js.map +1 -0
  144. package/dist/mcp/server.d.ts +7 -0
  145. package/dist/mcp/server.js +34 -0
  146. package/dist/mcp/server.js.map +1 -0
  147. package/dist/mcp/stdio.d.ts +7 -0
  148. package/dist/mcp/stdio.js +25 -0
  149. package/dist/mcp/stdio.js.map +1 -0
  150. package/dist/mcp/tools/echo.d.ts +7 -0
  151. package/dist/mcp/tools/echo.js +17 -0
  152. package/dist/mcp/tools/echo.js.map +1 -0
  153. package/dist/mcp/tools/sf-gql-aggregate.d.ts +11 -0
  154. package/dist/mcp/tools/sf-gql-aggregate.js +75 -0
  155. package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -0
  156. package/dist/mcp/tools/sf-gql-create.d.ts +11 -0
  157. package/dist/mcp/tools/sf-gql-create.js +35 -0
  158. package/dist/mcp/tools/sf-gql-create.js.map +1 -0
  159. package/dist/mcp/tools/sf-gql-delete.d.ts +11 -0
  160. package/dist/mcp/tools/sf-gql-delete.js +31 -0
  161. package/dist/mcp/tools/sf-gql-delete.js.map +1 -0
  162. package/dist/mcp/tools/sf-gql-detail.d.ts +11 -0
  163. package/dist/mcp/tools/sf-gql-detail.js +58 -0
  164. package/dist/mcp/tools/sf-gql-detail.js.map +1 -0
  165. package/dist/mcp/tools/sf-gql-discover.d.ts +9 -0
  166. package/dist/mcp/tools/sf-gql-discover.js +51 -0
  167. package/dist/mcp/tools/sf-gql-discover.js.map +1 -0
  168. package/dist/mcp/tools/sf-gql-list.d.ts +11 -0
  169. package/dist/mcp/tools/sf-gql-list.js +53 -0
  170. package/dist/mcp/tools/sf-gql-list.js.map +1 -0
  171. package/dist/mcp/tools/sf-gql-raw.d.ts +11 -0
  172. package/dist/mcp/tools/sf-gql-raw.js +39 -0
  173. package/dist/mcp/tools/sf-gql-raw.js.map +1 -0
  174. package/dist/mcp/tools/sf-gql-update.d.ts +11 -0
  175. package/dist/mcp/tools/sf-gql-update.js +35 -0
  176. package/dist/mcp/tools/sf-gql-update.js.map +1 -0
  177. package/dist/mcp/tools/shared/zod-schemas.d.ts +49 -0
  178. package/dist/mcp/tools/shared/zod-schemas.js +46 -0
  179. package/dist/mcp/tools/shared/zod-schemas.js.map +1 -0
  180. package/package.json +36 -0
  181. package/ralph-loop.sh +120 -0
  182. package/scripts/smoke-orderby.sh +190 -0
  183. package/src/__tests__/helpers/prime-deps.ts +46 -0
  184. package/src/__tests__/helpers/schema.ts +73 -0
  185. package/src/__tests__/helpers/stdout.ts +33 -0
  186. package/src/__tests__/setup.ts +19 -0
  187. package/src/cli.ts +764 -0
  188. package/src/commands/__tests__/query.spec.ts +137 -0
  189. package/src/commands/args.ts +306 -0
  190. package/src/commands/build.ts +288 -0
  191. package/src/commands/connect.ts +60 -0
  192. package/src/commands/describe.ts +246 -0
  193. package/src/commands/meta.ts +171 -0
  194. package/src/commands/navigate.ts +134 -0
  195. package/src/commands/query-helpers.ts +1202 -0
  196. package/src/commands/query.ts +1085 -0
  197. package/src/commands/review.ts +670 -0
  198. package/src/commands/session-mgmt.ts +256 -0
  199. package/src/commands/type.ts +437 -0
  200. package/src/commands/validate-input.ts +38 -0
  201. package/src/intent/__tests__/build-aggregate.spec.ts +931 -0
  202. package/src/intent/__tests__/build-create-validation.spec.ts +135 -0
  203. package/src/intent/__tests__/build-delete.spec.ts +121 -0
  204. package/src/intent/__tests__/build-detail.spec.ts +333 -0
  205. package/src/intent/__tests__/build-discover.spec.ts +432 -0
  206. package/src/intent/__tests__/build-list.spec.ts +284 -0
  207. package/src/intent/__tests__/build-mutation.spec.ts +108 -0
  208. package/src/intent/__tests__/build-output.spec.ts +55 -0
  209. package/src/intent/__tests__/build-raw.spec.ts +153 -0
  210. package/src/intent/__tests__/build-update-validation.spec.ts +134 -0
  211. package/src/intent/build-aggregate.ts +182 -0
  212. package/src/intent/build-create.ts +19 -0
  213. package/src/intent/build-delete.ts +62 -0
  214. package/src/intent/build-detail.ts +95 -0
  215. package/src/intent/build-discover.ts +199 -0
  216. package/src/intent/build-list.ts +91 -0
  217. package/src/intent/build-mutation.ts +75 -0
  218. package/src/intent/build-output.ts +72 -0
  219. package/src/intent/build-raw.ts +61 -0
  220. package/src/intent/build-update.ts +19 -0
  221. package/src/intent/get-schema-with-priming.ts +43 -0
  222. package/src/intent/select-child-relationship.ts +48 -0
  223. package/src/intent/types.ts +181 -0
  224. package/src/lib/__tests__/apply-command.parity.spec.ts +97 -0
  225. package/src/lib/__tests__/apply-command.spec.ts +171 -0
  226. package/src/lib/__tests__/auth.spec.ts +292 -0
  227. package/src/lib/__tests__/formatter.spec.ts +86 -0
  228. package/src/lib/__tests__/graphql-name.spec.ts +64 -0
  229. package/src/lib/__tests__/introspect.spec.ts +399 -0
  230. package/src/lib/__tests__/object-info.spec.ts +124 -0
  231. package/src/lib/__tests__/path-selection.spec.ts +219 -0
  232. package/src/lib/__tests__/prime-schema.spec.ts +269 -0
  233. package/src/lib/__tests__/query-builder.spec.ts +95 -0
  234. package/src/lib/__tests__/query-commands.spec.ts +74 -0
  235. package/src/lib/__tests__/session.spec.ts +292 -0
  236. package/src/lib/__tests__/tokenize.spec.ts +33 -0
  237. package/src/lib/__tests__/uiapi.spec.ts +192 -0
  238. package/src/lib/__tests__/variable-promotion.spec.ts +211 -0
  239. package/src/lib/__tests__/walker.spec.ts +250 -0
  240. package/src/lib/apply-command.ts +286 -0
  241. package/src/lib/auth.ts +157 -0
  242. package/src/lib/codegen.ts +899 -0
  243. package/src/lib/command-registry.ts +434 -0
  244. package/src/lib/formatter.ts +587 -0
  245. package/src/lib/fs-utils.ts +46 -0
  246. package/src/lib/graphql-name.ts +35 -0
  247. package/src/lib/interactive.ts +634 -0
  248. package/src/lib/introspect.ts +320 -0
  249. package/src/lib/object-info.ts +409 -0
  250. package/src/lib/path-selection.ts +162 -0
  251. package/src/lib/prime-schema.ts +195 -0
  252. package/src/lib/query-builder.ts +193 -0
  253. package/src/lib/query-commands.ts +290 -0
  254. package/src/lib/session.ts +1304 -0
  255. package/src/lib/tokenize.ts +43 -0
  256. package/src/lib/uiapi.ts +176 -0
  257. package/src/lib/validator.ts +143 -0
  258. package/src/lib/variable-promotion.ts +145 -0
  259. package/src/lib/walker.ts +975 -0
  260. package/src/mcp/__tests__/server.spec.ts +155 -0
  261. package/src/mcp/server.ts +38 -0
  262. package/src/mcp/stdio.ts +29 -0
  263. package/src/mcp/tools/__tests__/sf-gql-aggregate.spec.ts +173 -0
  264. package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +235 -0
  265. package/src/mcp/tools/__tests__/sf-gql-delete.spec.ts +194 -0
  266. package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +246 -0
  267. package/src/mcp/tools/__tests__/sf-gql-discover.spec.ts +320 -0
  268. package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +128 -0
  269. package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +141 -0
  270. package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +207 -0
  271. package/src/mcp/tools/echo.ts +24 -0
  272. package/src/mcp/tools/sf-gql-aggregate.ts +102 -0
  273. package/src/mcp/tools/sf-gql-create.ts +55 -0
  274. package/src/mcp/tools/sf-gql-delete.ts +49 -0
  275. package/src/mcp/tools/sf-gql-detail.ts +85 -0
  276. package/src/mcp/tools/sf-gql-discover.ts +67 -0
  277. package/src/mcp/tools/sf-gql-list.ts +73 -0
  278. package/src/mcp/tools/sf-gql-raw.ts +56 -0
  279. package/src/mcp/tools/sf-gql-update.ts +55 -0
  280. package/src/mcp/tools/shared/zod-schemas.ts +55 -0
  281. package/tsconfig.json +18 -0
  282. package/vitest.config.ts +14 -0
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ import { describe, expect, it } from "vitest";
8
+ import { makeSession } from "../../__tests__/helpers/schema.js";
9
+ import { renderQuery } from "../query-builder.js";
10
+ import {
11
+ addVariable,
12
+ appendListElement,
13
+ buildRuntimeVariables,
14
+ cloneSession,
15
+ createSiblingFieldInstance,
16
+ deepGetArg,
17
+ deepRemoveArg,
18
+ deepSetArg,
19
+ deepSetVariableValue,
20
+ getArgsFieldPath,
21
+ getInputSubPath,
22
+ getNavigationContext,
23
+ isInArgsContext,
24
+ loadSession,
25
+ parseVariablePath,
26
+ queryNavToSchemaPath,
27
+ removeListElement,
28
+ saveSession,
29
+ selectLeaf,
30
+ setAliasOnPath,
31
+ setArg,
32
+ } from "../session.js";
33
+
34
+ describe("session", () => {
35
+ describe("navigation context helpers", () => {
36
+ it("getNavigationContext returns correct contexts", () => {
37
+ expect(getNavigationContext([])).toBe("root");
38
+ expect(getNavigationContext(["query"])).toBe("query");
39
+ expect(getNavigationContext(["query", "uiapi"])).toBe("query");
40
+ expect(getNavigationContext(["variables"])).toBe("variables");
41
+ expect(getNavigationContext(["variables", "$filter"])).toBe("variables");
42
+ });
43
+
44
+ it("isInArgsContext detects @args segment", () => {
45
+ expect(isInArgsContext(["query", "accounts", "@args"])).toBe(true);
46
+ expect(isInArgsContext(["query", "accounts", "@args", "where"])).toBe(true);
47
+ expect(isInArgsContext(["query", "accounts"])).toBe(false);
48
+ expect(isInArgsContext([])).toBe(false);
49
+ });
50
+
51
+ it("getArgsFieldPath extracts field path before @args", () => {
52
+ expect(getArgsFieldPath(["query", "accounts", "@args", "where"])).toEqual(["accounts"]);
53
+ expect(getArgsFieldPath(["query", "viewer", "@args"])).toEqual(["viewer"]);
54
+ expect(getArgsFieldPath([])).toEqual([]);
55
+ });
56
+
57
+ it("getInputSubPath extracts path after @args", () => {
58
+ expect(getInputSubPath(["query", "accounts", "@args", "where", "name"])).toEqual([
59
+ "where",
60
+ "name",
61
+ ]);
62
+ expect(getInputSubPath(["query", "accounts", "@args"])).toEqual([]);
63
+ expect(getInputSubPath([])).toEqual([]);
64
+ });
65
+
66
+ it("queryNavToSchemaPath strips query prefix and stops at @args", () => {
67
+ expect(queryNavToSchemaPath(["query", "accounts"])).toEqual(["accounts"]);
68
+ expect(queryNavToSchemaPath(["query", "accounts", "@args", "where"])).toEqual(["accounts"]);
69
+ expect(queryNavToSchemaPath([])).toEqual([]);
70
+ expect(queryNavToSchemaPath(["variables"])).toEqual([]);
71
+ });
72
+
73
+ it("parseVariablePath extracts variable name and sub-path", () => {
74
+ const result = parseVariablePath(["variables", "$filter", "name"]);
75
+ expect(result).not.toBeNull();
76
+ expect(result!.varName).toBe("filter");
77
+ expect(result!.inputSubPath).toEqual(["name"]);
78
+
79
+ expect(parseVariablePath(["query"])).toBeNull();
80
+ expect(parseVariablePath(["variables"])).toBeNull();
81
+ });
82
+ });
83
+
84
+ describe("deep set/get helpers", () => {
85
+ it("deepSetArg creates nested JSON structure", () => {
86
+ const session = makeSession();
87
+ session.navigationPath = ["query", "accounts"];
88
+
89
+ setArg(session, ["accounts"], "first", "10");
90
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
91
+
92
+ deepSetArg(session, ["accounts"], "where", ["name", "like"], '"Acme%"');
93
+
94
+ const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!;
95
+ expect(raw.kind).toBe("field");
96
+ if (raw.kind !== "field") throw new Error("unreachable");
97
+ const parsed = JSON.parse(raw.args["where"]);
98
+ expect(parsed).toEqual({ name: { like: "Acme%" } });
99
+ });
100
+
101
+ it("deepSetArg merges with existing values", () => {
102
+ const session = makeSession();
103
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
104
+ deepSetArg(session, ["accounts"], "where", ["name", "like"], '"Acme%"');
105
+ deepSetArg(session, ["accounts"], "where", ["minRevenue"], "1000");
106
+
107
+ const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!;
108
+ expect(raw.kind).toBe("field");
109
+ if (raw.kind !== "field") throw new Error("unreachable");
110
+ const parsed = JSON.parse(raw.args["where"]);
111
+ expect(parsed).toEqual({ name: { like: "Acme%" }, minRevenue: 1000 });
112
+ });
113
+
114
+ it("deepSetArg with empty inputPath sets top-level arg directly", () => {
115
+ const session = makeSession();
116
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
117
+ deepSetArg(session, ["accounts"], "first", [], "10");
118
+
119
+ const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!;
120
+ expect(raw.kind).toBe("field");
121
+ if (raw.kind !== "field") throw new Error("unreachable");
122
+ expect(raw.args["first"]).toBe("10");
123
+ });
124
+
125
+ it("deepGetArg retrieves nested values", () => {
126
+ const session = makeSession();
127
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
128
+ deepSetArg(session, ["accounts"], "where", ["name", "like"], '"Acme%"');
129
+
130
+ const value = deepGetArg(session, ["accounts"], "where", ["name", "like"]);
131
+ expect(value).toBe("Acme%");
132
+
133
+ const whole = deepGetArg(session, ["accounts"], "where", []);
134
+ expect(typeof whole).toBe("string");
135
+ });
136
+
137
+ it("deepRemoveArg removes nested values", () => {
138
+ const session = makeSession();
139
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
140
+ deepSetArg(session, ["accounts"], "where", ["name", "like"], '"Acme%"');
141
+ deepSetArg(session, ["accounts"], "where", ["minRevenue"], "100");
142
+
143
+ const removed = deepRemoveArg(session, ["accounts"], "where", ["name", "like"]);
144
+ expect(removed).toBe(true);
145
+
146
+ const remaining = deepGetArg(session, ["accounts"], "where", ["minRevenue"]);
147
+ expect(remaining).toBe(100);
148
+ });
149
+
150
+ it("deepSetVariableValue builds nested variable runtime values", () => {
151
+ const session = makeSession();
152
+ addVariable(session, "filter", "AccountFilter");
153
+
154
+ deepSetVariableValue(session, "filter", ["name", "like"], '"Test%"');
155
+
156
+ const variable = session.variables.find((v) => v.name === "filter")!;
157
+ expect(variable.runtimeValue).toBeDefined();
158
+ const parsed = JSON.parse(variable.runtimeValue!);
159
+ expect(parsed).toEqual({ name: { like: "Test%" } });
160
+ });
161
+ });
162
+
163
+ describe("list element management", () => {
164
+ it("appendListElement creates array elements", () => {
165
+ const session = makeSession();
166
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
167
+
168
+ const idx0 = appendListElement(session, ["accounts"], "orderBy", []);
169
+ expect(idx0).toBe(0);
170
+
171
+ const idx1 = appendListElement(session, ["accounts"], "orderBy", []);
172
+ expect(idx1).toBe(1);
173
+
174
+ const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!;
175
+ expect(raw.kind).toBe("field");
176
+ if (raw.kind !== "field") throw new Error("unreachable");
177
+ const parsed = JSON.parse(raw.args["orderBy"]);
178
+ expect(Array.isArray(parsed)).toBe(true);
179
+ expect(parsed).toHaveLength(2);
180
+ });
181
+
182
+ it("removeListElement splices array", () => {
183
+ const session = makeSession();
184
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
185
+
186
+ appendListElement(session, ["accounts"], "orderBy", []);
187
+ appendListElement(session, ["accounts"], "orderBy", []);
188
+
189
+ const removed = removeListElement(session, ["accounts"], "orderBy", [], 0);
190
+ expect(removed).toBe(true);
191
+
192
+ const raw = session.nodes.find((n) => n.kind === "field" && n.fieldName === "accounts")!;
193
+ expect(raw.kind).toBe("field");
194
+ if (raw.kind !== "field") throw new Error("unreachable");
195
+ const parsed = JSON.parse(raw.args["orderBy"]);
196
+ expect(parsed).toHaveLength(1);
197
+ });
198
+ });
199
+
200
+ describe("session migration", () => {
201
+ it("loadSession auto-migrates old navigation paths", () => {
202
+ const session = makeSession();
203
+ session.navigationPath = ["accounts"]; // old-style path without "query" prefix
204
+ saveSession(session);
205
+
206
+ const loaded = loadSession(session.id);
207
+ expect(loaded.navigationPath).toEqual(["query", "accounts"]);
208
+ });
209
+
210
+ it("loadSession does not double-prefix already-migrated paths", () => {
211
+ const session = makeSession();
212
+ session.navigationPath = ["query", "accounts"];
213
+ saveSession(session);
214
+
215
+ const loaded = loadSession(session.id);
216
+ expect(loaded.navigationPath).toEqual(["query", "accounts"]);
217
+ });
218
+ });
219
+
220
+ describe("aliasing & cloning", () => {
221
+ it("supports multiple instances of the same field with different variables", () => {
222
+ const session = makeSession();
223
+
224
+ addVariable(session, "$limit", "Int");
225
+ addVariable(session, "$otherLimit", "Int");
226
+ addVariable(session, "$accountFilter", "AccountFilter");
227
+ addVariable(session, "$otherFilter", "AccountFilter");
228
+
229
+ session.navigationPath = ["query", "accounts"];
230
+ setAliasOnPath(session, ["accounts"], "highRevenueAccounts");
231
+ setArg(session, ["accounts"], "first", "$limit");
232
+ setArg(session, ["accounts"], "where", "$accountFilter");
233
+
234
+ session.navigationPath = ["query", "accounts", "edges", "node"];
235
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
236
+
237
+ session.navigationPath = ["query", "accounts"];
238
+ createSiblingFieldInstance(session, ["accounts"], "regionalAccounts");
239
+ setArg(session, ["accounts"], "first", "$otherLimit");
240
+ setArg(session, ["accounts"], "where", "$otherFilter");
241
+
242
+ session.navigationPath = ["query", "accounts", "edges", "node"];
243
+ selectLeaf(session, ["accounts", "edges", "node", "id"]);
244
+
245
+ // Assertion lives in query-builder.spec.ts; here we only verify session state changes.
246
+ const aliased = session.nodes.filter((n) => n.kind === "field" && n.fieldName === "accounts");
247
+ expect(aliased.length).toBeGreaterThanOrEqual(2);
248
+ });
249
+
250
+ it("cloneSession produces a deep copy with a new id", () => {
251
+ const session = makeSession();
252
+ addVariable(session, "limit", "Int", "10");
253
+ session.navigationPath = ["query", "accounts"];
254
+ setAliasOnPath(session, ["accounts"], "myAccounts");
255
+ setArg(session, ["accounts"], "first", "5");
256
+
257
+ session.navigationPath = ["query", "accounts", "edges", "node"];
258
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
259
+
260
+ const cloned = cloneSession(session, "my-clone");
261
+
262
+ expect(cloned.id).not.toBe(session.id);
263
+ expect(cloned.name).toBe("my-clone");
264
+ expect(cloned.variables).toHaveLength(session.variables.length);
265
+ expect(cloned.nodes).toHaveLength(session.nodes.length);
266
+ expect(renderQuery(cloned)).toBe(renderQuery(session));
267
+
268
+ addVariable(cloned, "extra", "String");
269
+ expect(cloned.variables.length).not.toBe(session.variables.length);
270
+ });
271
+ });
272
+
273
+ describe("runtime variable building", () => {
274
+ it("runtime variables are built from runtime values and defaults", () => {
275
+ const session = makeSession();
276
+
277
+ addVariable(session, "$limit", "Int", "10");
278
+ addVariable(session, "$filters", "AccountFilter");
279
+ addVariable(session, "$isActive", "Boolean");
280
+
281
+ session.variables.find((variable) => variable.name === "filters")!.runtimeValue =
282
+ '{"minRevenue":1000}';
283
+ session.variables.find((variable) => variable.name === "isActive")!.runtimeValue = "true";
284
+
285
+ expect(buildRuntimeVariables(session)).toEqual({
286
+ limit: 10,
287
+ filters: { minRevenue: 1000 },
288
+ isActive: true,
289
+ });
290
+ });
291
+ });
292
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ import { describe, expect, it } from "vitest";
8
+ import { tokenizeCommand } from "../tokenize.js";
9
+
10
+ describe("lib/tokenize", () => {
11
+ it("splits on unquoted spaces", () => {
12
+ expect(tokenizeCommand("select Id Name")).toEqual(["select", "Id", "Name"]);
13
+ });
14
+
15
+ it("keeps single-quoted segments intact", () => {
16
+ expect(tokenizeCommand('set where=\'{"Status":{"eq":"New"}}\'')).toEqual([
17
+ "set",
18
+ 'where=\'{"Status":{"eq":"New"}}\'',
19
+ ]);
20
+ });
21
+
22
+ it("keeps double-quoted segments intact", () => {
23
+ expect(tokenizeCommand('set @args/where/Name/like "Acme%"')).toEqual([
24
+ "set",
25
+ "@args/where/Name/like",
26
+ "Acme%",
27
+ ]);
28
+ });
29
+
30
+ it("returns empty array for blank input", () => {
31
+ expect(tokenizeCommand(" ")).toEqual([]);
32
+ });
33
+ });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ import { buildSchema } from "graphql";
8
+ import { describe, expect, it } from "vitest";
9
+ import {
10
+ aggregatePath,
11
+ connectionNodePath,
12
+ connectionPath,
13
+ createInputTypeName,
14
+ filterTypeName,
15
+ isValueWrapperType,
16
+ mutationFieldPath,
17
+ mutationRecordPath,
18
+ nodePath,
19
+ orderByTypeName,
20
+ pageInfoPath,
21
+ resolveScalarSelectionPath,
22
+ updateInputTypeName,
23
+ } from "../uiapi.js";
24
+
25
+ describe("uiapi", () => {
26
+ describe("path constructors", () => {
27
+ it("connectionPath builds the uiapi.query.<Object> path", () => {
28
+ expect(connectionPath("Account")).toEqual(["uiapi", "query", "Account"]);
29
+ });
30
+
31
+ it("nodePath descends through edges/node from the connection", () => {
32
+ expect(nodePath("Account")).toEqual(["uiapi", "query", "Account", "edges", "node"]);
33
+ });
34
+
35
+ it("connectionNodePath works on any connection path (incl. nested children)", () => {
36
+ const nestedConnection = ["uiapi", "query", "Account", "edges", "node", "Cases"];
37
+ expect(connectionNodePath(nestedConnection)).toEqual([
38
+ "uiapi",
39
+ "query",
40
+ "Account",
41
+ "edges",
42
+ "node",
43
+ "Cases",
44
+ "edges",
45
+ "node",
46
+ ]);
47
+ });
48
+
49
+ it("pageInfoPath sits at the connection root", () => {
50
+ expect(pageInfoPath(["uiapi", "query", "Account"])).toEqual([
51
+ "uiapi",
52
+ "query",
53
+ "Account",
54
+ "pageInfo",
55
+ ]);
56
+ });
57
+
58
+ it("mutationFieldPath builds uiapi.<Object><Op> for each mutation kind", () => {
59
+ expect(mutationFieldPath("Account", "Create")).toEqual(["uiapi", "AccountCreate"]);
60
+ expect(mutationFieldPath("Account", "Update")).toEqual(["uiapi", "AccountUpdate"]);
61
+ expect(mutationFieldPath("Account", "Delete")).toEqual(["uiapi", "AccountDelete"]);
62
+ });
63
+
64
+ it("mutationRecordPath descends into the Record selection scope", () => {
65
+ expect(mutationRecordPath("Account", "Create")).toEqual(["uiapi", "AccountCreate", "Record"]);
66
+ });
67
+
68
+ it("aggregatePath builds the aggregate root", () => {
69
+ expect(aggregatePath("Account")).toEqual(["uiapi", "aggregate", "Account"]);
70
+ });
71
+ });
72
+
73
+ describe("naming conventions", () => {
74
+ it("filterTypeName / orderByTypeName / *InputTypeName follow Salesforce conventions", () => {
75
+ expect(filterTypeName("Account")).toBe("Account_Filter");
76
+ expect(orderByTypeName("Account")).toBe("Account_OrderBy");
77
+ expect(createInputTypeName("Account")).toBe("AccountCreateInput");
78
+ expect(updateInputTypeName("Account")).toBe("AccountUpdateInput");
79
+ });
80
+ });
81
+
82
+ describe("isValueWrapperType", () => {
83
+ const wrapperSchema = buildSchema(`
84
+ type Query {
85
+ any: Holder
86
+ }
87
+
88
+ type Holder {
89
+ Id: ID!
90
+ Name: StringValue
91
+ Status: PicklistValue
92
+ # A *Value-named type that does NOT have a value field — the
93
+ # detection helper must reject this to avoid false positives.
94
+ ImpostorValue: Impostor
95
+ }
96
+
97
+ type StringValue { value: String }
98
+ type PicklistValue { value: String }
99
+ type Impostor { other: String }
100
+ `);
101
+
102
+ it("detects StringValue and PicklistValue", () => {
103
+ expect(isValueWrapperType(wrapperSchema, "StringValue")).toBe(true);
104
+ expect(isValueWrapperType(wrapperSchema, "PicklistValue")).toBe(true);
105
+ });
106
+
107
+ it("rejects *Value types that don't expose a `value` field", () => {
108
+ expect(isValueWrapperType(wrapperSchema, "Impostor")).toBe(false);
109
+ });
110
+
111
+ it("returns false for unknown / non-object types", () => {
112
+ expect(isValueWrapperType(wrapperSchema, "DoesNotExist")).toBe(false);
113
+ expect(isValueWrapperType(wrapperSchema, "ID")).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe("resolveScalarSelectionPath", () => {
118
+ const sobjectSchema = buildSchema(`
119
+ type Query {
120
+ uiapi: UIAPI!
121
+ }
122
+
123
+ type UIAPI {
124
+ query: RecordQuery!
125
+ }
126
+
127
+ type RecordQuery {
128
+ Account: AccountConnection!
129
+ }
130
+
131
+ type AccountConnection { edges: [AccountEdge!]! }
132
+ type AccountEdge { node: Account! }
133
+
134
+ type Account {
135
+ Id: ID!
136
+ Name: StringValue
137
+ Industry: StringValue
138
+ Owner: OwnerUnion
139
+ }
140
+
141
+ union OwnerUnion = User | Group
142
+ type User { Id: ID! }
143
+ type Group { Id: ID! }
144
+
145
+ type StringValue { value: String }
146
+ `);
147
+
148
+ const accountNodePath = ["uiapi", "query", "Account", "edges", "node"];
149
+
150
+ it("returns the leaf path for Id (no value wrapper)", () => {
151
+ expect(resolveScalarSelectionPath(sobjectSchema, "query", accountNodePath, "Id")).toEqual([
152
+ "uiapi",
153
+ "query",
154
+ "Account",
155
+ "edges",
156
+ "node",
157
+ "Id",
158
+ ]);
159
+ });
160
+
161
+ it("appends 'value' for value-wrapper fields", () => {
162
+ expect(resolveScalarSelectionPath(sobjectSchema, "query", accountNodePath, "Name")).toEqual([
163
+ "uiapi",
164
+ "query",
165
+ "Account",
166
+ "edges",
167
+ "node",
168
+ "Name",
169
+ "value",
170
+ ]);
171
+ });
172
+
173
+ it("returns the field path for non-leaf, non-wrapper types", () => {
174
+ // Owner is a union — the helper returns the bare path; callers that
175
+ // need union expansion use selectDottedFieldPath from path-selection.ts.
176
+ expect(resolveScalarSelectionPath(sobjectSchema, "query", accountNodePath, "Owner")).toEqual([
177
+ "uiapi",
178
+ "query",
179
+ "Account",
180
+ "edges",
181
+ "node",
182
+ "Owner",
183
+ ]);
184
+ });
185
+
186
+ it("returns null when the field can't be resolved", () => {
187
+ expect(
188
+ resolveScalarSelectionPath(sobjectSchema, "query", accountNodePath, "NotARealField"),
189
+ ).toBeNull();
190
+ });
191
+ });
192
+ });