@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,219 @@
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 { selectDottedFieldPath } from "../path-selection.js";
10
+ import { renderQuery } from "../query-builder.js";
11
+ import { createSession, type QuerySession } from "../session.js";
12
+ import { MutationContextError } from "../walker.js";
13
+
14
+ // Models a Salesforce-shaped UIAPI with:
15
+ // - Value wrapper types (StringValue { value: String }) on scalar fields
16
+ // - A polymorphic Owner union (User | Group) — User has Email, Group does not
17
+ // - A scalar Id field that is NOT wrapped (matches UIAPI convention)
18
+ // - A non-polymorphic Account parent (plain object) for the control case
19
+ const schema = buildSchema(`
20
+ type Query {
21
+ uiapi: UIAPI!
22
+ }
23
+
24
+ type UIAPI {
25
+ query: RecordQuery!
26
+ }
27
+
28
+ type RecordQuery {
29
+ Account(first: Int): AccountConnection!
30
+ Case(first: Int): CaseConnection!
31
+ }
32
+
33
+ type AccountConnection { edges: [AccountEdge!]! }
34
+ type AccountEdge { node: Account! }
35
+
36
+ type CaseConnection { edges: [CaseEdge!]! }
37
+ type CaseEdge { node: Case! }
38
+
39
+ type Account {
40
+ Id: ID!
41
+ Name: StringValue
42
+ Industry: StringValue
43
+ Owner: OwnerUnion
44
+ }
45
+
46
+ type Case {
47
+ Id: ID!
48
+ Subject: StringValue
49
+ Owner: OwnerUnion
50
+ Account: Account
51
+ }
52
+
53
+ union OwnerUnion = User | Group
54
+
55
+ type User {
56
+ Id: ID!
57
+ Name: StringValue
58
+ Email: StringValue
59
+ }
60
+
61
+ type Group {
62
+ Id: ID!
63
+ Name: StringValue
64
+ }
65
+
66
+ type StringValue {
67
+ value: String
68
+ }
69
+ `);
70
+
71
+ function makeSession(): QuerySession {
72
+ const session = createSession("test-org", "query");
73
+ session.navigationPath = ["query"];
74
+ return session;
75
+ }
76
+
77
+ const accountNodePath = ["uiapi", "query", "Account", "edges", "node"];
78
+ const caseNodePath = ["uiapi", "query", "Case", "edges", "node"];
79
+
80
+ describe("path-selection", () => {
81
+ describe("non-polymorphic dotted path", () => {
82
+ it("selects a scalar through a plain parent relationship", () => {
83
+ const session = makeSession();
84
+
85
+ selectDottedFieldPath(session, schema, caseNodePath, "Account.Name");
86
+
87
+ const query = renderQuery(session);
88
+ expect(query).toMatch(/Account\b[\s\S]*Name[\s\S]*\{[\s\S]*value/);
89
+ // Should NOT introduce inline fragments — Account is a plain object.
90
+ expect(query).not.toMatch(/\.\.\. on /);
91
+ });
92
+
93
+ it("selects Id without appending .value", () => {
94
+ const session = makeSession();
95
+
96
+ selectDottedFieldPath(session, schema, accountNodePath, "Id");
97
+
98
+ const query = renderQuery(session);
99
+ // Id is a scalar directly under Account; no `value` wrapper.
100
+ expect(query).toMatch(/Id\b/);
101
+ expect(query).not.toMatch(/Id \{[\s\S]*value/);
102
+ });
103
+
104
+ it("unwraps a StringValue to its .value selection", () => {
105
+ const session = makeSession();
106
+
107
+ selectDottedFieldPath(session, schema, accountNodePath, "Name");
108
+
109
+ const query = renderQuery(session);
110
+ // Name is a value wrapper — must select Name { value }.
111
+ expect(query).toMatch(/Name \{[\s\S]*value/);
112
+ });
113
+ });
114
+
115
+ describe("polymorphic union expansion", () => {
116
+ it("expands a polymorphic union segment to inline fragments", () => {
117
+ const session = makeSession();
118
+
119
+ selectDottedFieldPath(session, schema, accountNodePath, "Owner.Name");
120
+
121
+ const query = renderQuery(session);
122
+ // Both members of OwnerUnion (User and Group) have Name → both selected.
123
+ expect(query).toMatch(/Owner \{[\s\S]*\.\.\. on User \{[\s\S]*Name \{[\s\S]*value/);
124
+ expect(query).toMatch(/Owner \{[\s\S]*\.\.\. on Group \{[\s\S]*Name \{[\s\S]*value/);
125
+ });
126
+
127
+ it("skips union members that lack the field", () => {
128
+ const session = makeSession();
129
+
130
+ // Email exists on User but NOT on Group — we should select on User only,
131
+ // silently skipping Group rather than failing the whole call.
132
+ selectDottedFieldPath(session, schema, accountNodePath, "Owner.Email");
133
+
134
+ const query = renderQuery(session);
135
+ expect(query).toMatch(/\.\.\. on User \{[\s\S]*Email \{[\s\S]*value/);
136
+ expect(query).not.toMatch(/\.\.\. on Group \{[\s\S]*Email/);
137
+ });
138
+
139
+ it("throws when no union member has the field", () => {
140
+ const session = makeSession();
141
+
142
+ // BadField exists on neither User nor Group. Without a guard the function
143
+ // would silently no-op and the caller would render `query { }`. Fail loudly.
144
+ expect(() =>
145
+ selectDottedFieldPath(session, schema, accountNodePath, "Owner.BadField"),
146
+ ).toThrow(/Field "BadField" not found on any member of union OwnerUnion/);
147
+ });
148
+
149
+ it("does not collide with sibling selections", () => {
150
+ const session = makeSession();
151
+
152
+ // Mixed selection: a non-polymorphic field, then a polymorphic one. The
153
+ // rendered query must contain both, and the union expansion must not
154
+ // disrupt the simple selection above it.
155
+ selectDottedFieldPath(session, schema, accountNodePath, "Name");
156
+ selectDottedFieldPath(session, schema, accountNodePath, "Owner.Name");
157
+
158
+ const query = renderQuery(session);
159
+ expect(query).toMatch(/Name \{[\s\S]*value/);
160
+ expect(query).toMatch(/Owner \{[\s\S]*\.\.\. on User/);
161
+ expect(query).toMatch(/Owner \{[\s\S]*\.\.\. on Group/);
162
+ });
163
+ });
164
+
165
+ describe("mutation context — MutationContextError", () => {
166
+ const mutationSchema = buildSchema(`
167
+ type Query { _placeholder: Boolean }
168
+ type Mutation { uiapi(input: UIAPIMutationsInput): UIAPIMutations! }
169
+ input UIAPIMutationsInput { allOrNone: Boolean }
170
+ type UIAPIMutations {
171
+ AccountCreate(input: AccountCreateInput!): AccountCreatePayload
172
+ }
173
+ input AccountCreateInput { Account: AccountCreateRepresentation! }
174
+ input AccountCreateRepresentation { Name: String }
175
+ type AccountCreatePayload { Record: Account }
176
+ type Account { Id: ID!, Name: StringValue, Owner: User }
177
+ type User { Id: ID!, Name: StringValue }
178
+ type StringValue { value: String }
179
+ `);
180
+
181
+ const mutationRecordPath = ["uiapi", "AccountCreate", "Record"];
182
+
183
+ function makeMutationSession(): QuerySession {
184
+ return createSession("test-org", "mutation");
185
+ }
186
+
187
+ it("throws MutationContextError for relationship field in mutation result", () => {
188
+ const session = makeMutationSession();
189
+
190
+ expect(() =>
191
+ selectDottedFieldPath(session, mutationSchema, mutationRecordPath, "Owner.Name"),
192
+ ).toThrow(MutationContextError);
193
+ });
194
+
195
+ it("MutationContextError message includes field name and guidance", () => {
196
+ const session = makeMutationSession();
197
+
198
+ try {
199
+ selectDottedFieldPath(session, mutationSchema, mutationRecordPath, "Owner.Name");
200
+ expect.fail("should have thrown");
201
+ } catch (err) {
202
+ expect(err).toBeInstanceOf(MutationContextError);
203
+ expect((err as MutationContextError).message).toMatch(/Owner/);
204
+ expect((err as MutationContextError).message).toMatch(/not available in mutation results/);
205
+ }
206
+ });
207
+
208
+ it("does not throw MutationContextError for scalar fields in mutation result", () => {
209
+ const session = makeMutationSession();
210
+
211
+ expect(() =>
212
+ selectDottedFieldPath(session, mutationSchema, mutationRecordPath, "Name"),
213
+ ).not.toThrow();
214
+
215
+ const query = renderQuery(session);
216
+ expect(query).toMatch(/Name \{[\s\S]*value/);
217
+ });
218
+ });
219
+ });
@@ -0,0 +1,269 @@
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 fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ describe("lib/prime-schema", () => {
13
+ it("atomic-write: temp file is renamed to final path, no .tmp left behind", async () => {
14
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-prime-"));
15
+ const finalPath = path.join(tmpRoot, "abc123.json");
16
+
17
+ const { atomicWriteJson } = await import("../fs-utils.js");
18
+
19
+ atomicWriteJson(finalPath, { hello: "world" });
20
+
21
+ expect(fs.existsSync(finalPath)).toBe(true);
22
+ expect(JSON.parse(fs.readFileSync(finalPath, "utf-8"))).toEqual({ hello: "world" });
23
+
24
+ const leftovers = fs.readdirSync(tmpRoot).filter((f) => f.includes(".tmp"));
25
+ expect(leftovers).toEqual([]);
26
+
27
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
28
+ });
29
+
30
+ it("atomic-write: parallel calls in the same process do not collide", async () => {
31
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-prime-parallel-"));
32
+ const finalPath = path.join(tmpRoot, "p.json");
33
+
34
+ const { atomicWriteJson } = await import("../fs-utils.js");
35
+
36
+ // 16 parallel writers — would surface a temp-name collision quickly.
37
+ await Promise.all(
38
+ Array.from({ length: 16 }, (_, i) =>
39
+ Promise.resolve().then(() => atomicWriteJson(finalPath, { i })),
40
+ ),
41
+ );
42
+
43
+ expect(fs.existsSync(finalPath)).toBe(true);
44
+ const leftovers = fs.readdirSync(tmpRoot).filter((f) => f.includes(".tmp"));
45
+ expect(leftovers).toEqual([]);
46
+ const contents = JSON.parse(fs.readFileSync(finalPath, "utf-8"));
47
+ expect(typeof contents.i).toBe("number");
48
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
49
+ });
50
+
51
+ it("lock: holder runs work, lock dir cleaned up after", async () => {
52
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-lock-"));
53
+ const finalPath = path.join(tmpRoot, "x.json");
54
+ const { withSchemaLock } = await import("../prime-schema.js");
55
+
56
+ let ran = false;
57
+ await withSchemaLock(finalPath, async () => {
58
+ ran = true;
59
+ expect(fs.existsSync(`${finalPath}.lock`)).toBe(true);
60
+ });
61
+
62
+ expect(ran).toBe(true);
63
+ expect(fs.existsSync(`${finalPath}.lock`)).toBe(false);
64
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
65
+ });
66
+
67
+ it("lock: cleanup tolerates stray files inside the lock dir (e.g. .DS_Store)", async () => {
68
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-lock-stray-"));
69
+ const finalPath = path.join(tmpRoot, "stray.json");
70
+ const { withSchemaLock } = await import("../prime-schema.js");
71
+
72
+ await withSchemaLock(finalPath, async () => {
73
+ // Simulate a stray file appearing inside the lock dir mid-work
74
+ // (macOS Finder, an editor swap, an OS indexer, etc.).
75
+ fs.writeFileSync(path.join(`${finalPath}.lock`, ".DS_Store"), "stray");
76
+ });
77
+
78
+ expect(fs.existsSync(`${finalPath}.lock`)).toBe(false);
79
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
80
+ });
81
+
82
+ it("lock: lock survives errors in the work function", async () => {
83
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-lock-err-"));
84
+ const finalPath = path.join(tmpRoot, "y.json");
85
+ const { withSchemaLock } = await import("../prime-schema.js");
86
+
87
+ await expect(
88
+ withSchemaLock(finalPath, async () => {
89
+ throw new Error("boom");
90
+ }),
91
+ ).rejects.toThrow(/boom/);
92
+ expect(fs.existsSync(`${finalPath}.lock`)).toBe(false);
93
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
94
+ });
95
+
96
+ it("lock: second waiter short-circuits when first holder primed the cache", async () => {
97
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-lock-race-"));
98
+ const finalPath = path.join(tmpRoot, "z.json");
99
+ const { withSchemaLock } = await import("../prime-schema.js");
100
+ const { atomicWriteJson } = await import("../fs-utils.js");
101
+
102
+ let firstStarted = false;
103
+ let firstFinishedWriting = false;
104
+ let secondWorkRan = false;
105
+
106
+ const first = withSchemaLock(finalPath, async () => {
107
+ firstStarted = true;
108
+ await new Promise((r) => setTimeout(r, 200));
109
+ atomicWriteJson(finalPath, { primed: true });
110
+ firstFinishedWriting = true;
111
+ });
112
+
113
+ // Wait for first to acquire before launching second.
114
+ while (!firstStarted) await new Promise((r) => setTimeout(r, 5));
115
+
116
+ const second = withSchemaLock(finalPath, async () => {
117
+ secondWorkRan = true;
118
+ return "ran" as const;
119
+ });
120
+
121
+ const [, secondResult] = await Promise.all([first, second]);
122
+ expect(firstFinishedWriting).toBe(true);
123
+ expect(secondWorkRan).toBe(false);
124
+ expect(secondResult).toBeUndefined();
125
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
126
+ });
127
+
128
+ it("lock: stale lock dir older than STALE_LOCK_MS is reclaimed", async () => {
129
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-lock-stale-"));
130
+ const finalPath = path.join(tmpRoot, "w.json");
131
+ const lockPath = `${finalPath}.lock`;
132
+ fs.mkdirSync(lockPath);
133
+ // Backdate the lock dir's mtime to well past the 7-min stale threshold.
134
+ const past = new Date(Date.now() - 10 * 60_000);
135
+ fs.utimesSync(lockPath, past, past);
136
+
137
+ const { withSchemaLock } = await import("../prime-schema.js");
138
+ let ran = false;
139
+ await withSchemaLock(finalPath, async () => {
140
+ ran = true;
141
+ });
142
+ expect(ran).toBe(true);
143
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
144
+ });
145
+
146
+ it("primeSchemaWithLock: returns metadata + duration on first prime", async () => {
147
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-prime-pub-"));
148
+ process.env.GRAPHITI_HOME = tmpRoot;
149
+
150
+ try {
151
+ const { primeSchemaWithLock } = await import("../prime-schema.js");
152
+
153
+ let downloadCalls = 0;
154
+ const stubDeps = {
155
+ getOrgAuth: async () => ({
156
+ alias: "test-org",
157
+ instanceUrl: "https://example.my.salesforce.com",
158
+ accessToken: "fake-token",
159
+ username: "user@example.com",
160
+ orgId: "00Dxx0000000000",
161
+ }),
162
+ downloadSchema: async () => {
163
+ downloadCalls++;
164
+ // downloadSchema's real impl writes the cache file. Simulate that.
165
+ const { schemaCacheKeyForInstanceUrl } = await import("../introspect.js");
166
+ const { atomicWriteJson } = await import("../fs-utils.js");
167
+ const cacheKey = schemaCacheKeyForInstanceUrl("https://example.my.salesforce.com");
168
+ atomicWriteJson(path.join(tmpRoot, "schemas", `${cacheKey}.json`), {
169
+ data: { __schema: { types: [] } },
170
+ __graphiti: {
171
+ instanceUrl: "https://example.my.salesforce.com",
172
+ alias: "test-org",
173
+ cachedAt: new Date().toISOString(),
174
+ },
175
+ });
176
+ return {
177
+ cacheKey,
178
+ instanceUrl: "https://example.my.salesforce.com",
179
+ typeCount: 0,
180
+ downloadedAt: new Date().toISOString(),
181
+ filePath: path.join(tmpRoot, "schemas", `${cacheKey}.json`),
182
+ };
183
+ },
184
+ };
185
+
186
+ const result = await primeSchemaWithLock("test-org", stubDeps);
187
+
188
+ expect(result.cached).toBe(false);
189
+ expect(typeof result.durationMs).toBe("number");
190
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
191
+ expect(result.filePath).toMatch(/\.json$/);
192
+ expect(fs.existsSync(result.filePath)).toBe(true);
193
+ expect(downloadCalls).toBe(1);
194
+
195
+ // Second call must be a cache hit, not a re-introspection.
196
+ const second = await primeSchemaWithLock("test-org", stubDeps);
197
+ expect(second.cached).toBe(true);
198
+ expect(second.durationMs).toBe(0);
199
+ expect(downloadCalls).toBe(1);
200
+ expect(second.instanceUrl).toBe("https://example.my.salesforce.com");
201
+ } finally {
202
+ delete process.env.GRAPHITI_HOME;
203
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
204
+ }
205
+ });
206
+
207
+ it("primeSchemaWithLock: surfaces auth-missing error verbatim", async () => {
208
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-prime-noauth-"));
209
+ process.env.GRAPHITI_HOME = tmpRoot;
210
+
211
+ try {
212
+ const { primeSchemaWithLock } = await import("../prime-schema.js");
213
+
214
+ const stubDeps = {
215
+ getOrgAuth: async () => {
216
+ throw new Error(
217
+ 'Unknown org "nonexistent-org". Run `sf org login web -a nonexistent-org`.',
218
+ );
219
+ },
220
+ downloadSchema: async () => {
221
+ throw new Error("should not be called");
222
+ },
223
+ };
224
+
225
+ await expect(primeSchemaWithLock("nonexistent-org", stubDeps)).rejects.toThrow(
226
+ /unknown org|sf org login/i,
227
+ );
228
+ } finally {
229
+ delete process.env.GRAPHITI_HOME;
230
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
231
+ }
232
+ });
233
+
234
+ it("primeSchemaWithLock: awaits an async getOrgAuth and returns its instanceUrl", async () => {
235
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "graphiti-prime-async-"));
236
+ process.env.GRAPHITI_HOME = tmpRoot;
237
+ try {
238
+ const { primeSchemaWithLock } = await import("../prime-schema.js");
239
+ const stubDeps = {
240
+ getOrgAuth: async () => ({
241
+ alias: "o",
242
+ username: "u",
243
+ instanceUrl: "https://o.my.salesforce.com",
244
+ accessToken: "t",
245
+ orgId: "00D",
246
+ }),
247
+ downloadSchema: async () => {
248
+ const { schemaCacheKeyForInstanceUrl } = await import("../introspect.js");
249
+ const { atomicWriteJson } = await import("../fs-utils.js");
250
+ const cacheKey = schemaCacheKeyForInstanceUrl("https://o.my.salesforce.com");
251
+ const filePath = path.join(tmpRoot, "schemas", `${cacheKey}.json`);
252
+ atomicWriteJson(filePath, { data: { __schema: { types: [] } } });
253
+ return {
254
+ cacheKey,
255
+ instanceUrl: "https://o.my.salesforce.com",
256
+ typeCount: 0,
257
+ downloadedAt: new Date().toISOString(),
258
+ filePath,
259
+ };
260
+ },
261
+ };
262
+ const result = await primeSchemaWithLock("o", stubDeps);
263
+ expect(result.instanceUrl).toBe("https://o.my.salesforce.com");
264
+ } finally {
265
+ delete process.env.GRAPHITI_HOME;
266
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
267
+ }
268
+ });
269
+ });
@@ -0,0 +1,95 @@
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, TEST_SCHEMA } from "../../__tests__/helpers/schema.js";
9
+ import { selectLeafInSession } from "../../commands/query.js";
10
+ import { renderQuery } from "../query-builder.js";
11
+ import {
12
+ addVariable,
13
+ createSiblingFieldInstance,
14
+ selectLeaf,
15
+ setAliasOnPath,
16
+ setArg,
17
+ } from "../session.js";
18
+ import { resolvePath } from "../walker.js";
19
+
20
+ describe("query-builder", () => {
21
+ describe("integration with walker (fragment validation)", () => {
22
+ it("fragment directories validate compatibility during navigation", () => {
23
+ const session = makeSession();
24
+
25
+ expect(resolvePath(TEST_SCHEMA, "query", ["search", "[Account]"]).typeName).toBe("Account");
26
+ expect(() => resolvePath(TEST_SCHEMA, "query", ["search", "[Viewer]"])).toThrow(
27
+ /not a possible type of union SearchHit/,
28
+ );
29
+
30
+ session.navigationPath = ["query", "search", "[Account]"];
31
+ selectLeafInSession(session, "name");
32
+
33
+ expect(renderQuery(session)).toMatch(/\.\.\. on Account \{\n\s+name\n\s+\}/);
34
+ });
35
+ });
36
+
37
+ describe("inline fragment dot-paths", () => {
38
+ it("supports inline fragment syntax for union types", () => {
39
+ const session = makeSession();
40
+ session.navigationPath = ["query", "search"];
41
+
42
+ selectLeafInSession(session, "[Account].name", "accountName");
43
+
44
+ const query = renderQuery(session);
45
+ expect(query).toMatch(/\.\.\. on Account \{/);
46
+ expect(query).toMatch(/accountName: name/);
47
+ });
48
+
49
+ it("supports nested inline fragment dot-paths", () => {
50
+ const session = makeSession();
51
+ session.navigationPath = ["query"];
52
+
53
+ selectLeafInSession(session, "search.[Contact].email");
54
+
55
+ const query = renderQuery(session);
56
+ expect(query).toMatch(/\.\.\. on Contact \{/);
57
+ expect(query).toMatch(/email/);
58
+ });
59
+ });
60
+
61
+ describe("aliasing rendering", () => {
62
+ it("renders multiple aliased instances with different variables", () => {
63
+ const session = makeSession();
64
+
65
+ addVariable(session, "$limit", "Int");
66
+ addVariable(session, "$otherLimit", "Int");
67
+ addVariable(session, "$accountFilter", "AccountFilter");
68
+ addVariable(session, "$otherFilter", "AccountFilter");
69
+
70
+ session.navigationPath = ["query", "accounts"];
71
+ setAliasOnPath(session, ["accounts"], "highRevenueAccounts");
72
+ setArg(session, ["accounts"], "first", "$limit");
73
+ setArg(session, ["accounts"], "where", "$accountFilter");
74
+
75
+ session.navigationPath = ["query", "accounts", "edges", "node"];
76
+ selectLeaf(session, ["accounts", "edges", "node", "name"]);
77
+
78
+ session.navigationPath = ["query", "accounts"];
79
+ createSiblingFieldInstance(session, ["accounts"], "regionalAccounts");
80
+ setArg(session, ["accounts"], "first", "$otherLimit");
81
+ setArg(session, ["accounts"], "where", "$otherFilter");
82
+
83
+ session.navigationPath = ["query", "accounts", "edges", "node"];
84
+ selectLeaf(session, ["accounts", "edges", "node", "id"]);
85
+
86
+ const query = renderQuery(session);
87
+ expect(query).toMatch(
88
+ /highRevenueAccounts: accounts\(first: \$limit, where: \$accountFilter\)/,
89
+ );
90
+ expect(query).toMatch(
91
+ /regionalAccounts: accounts\(first: \$otherLimit, where: \$otherFilter\)/,
92
+ );
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,74 @@
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 { formatHelp, formatNextSteps, QUERY_COMMANDS } from "../query-commands.js";
10
+
11
+ describe("query-commands", () => {
12
+ describe("command registry", () => {
13
+ it("lists all expected commands", () => {
14
+ const names = QUERY_COMMANDS.map((c) => c.name);
15
+ for (const expected of [
16
+ "pwd",
17
+ "ls",
18
+ "cd",
19
+ "select",
20
+ "drop",
21
+ "set",
22
+ "alias",
23
+ "var",
24
+ "show",
25
+ "check",
26
+ "run",
27
+ "help",
28
+ "interactive",
29
+ ]) {
30
+ expect(names).toContain(expected);
31
+ }
32
+ });
33
+
34
+ it("every command spec has a non-empty summary and usage", () => {
35
+ for (const spec of QUERY_COMMANDS) {
36
+ expect(spec.summary.length).toBeGreaterThan(0);
37
+ expect(spec.usage.length).toBeGreaterThan(0);
38
+ }
39
+ });
40
+ });
41
+
42
+ describe("formatHelp", () => {
43
+ it("with no topic returns a full listing", () => {
44
+ const help = formatHelp();
45
+ expect(help).toContain("pwd");
46
+ expect(help).toContain("select");
47
+ expect(help).toContain("interactive");
48
+ expect(help).toContain("Run `help <subcommand>`");
49
+ });
50
+
51
+ it("with known topic returns usage and examples", () => {
52
+ const help = formatHelp("set");
53
+ expect(help).toContain("set");
54
+ expect(help).toContain("Usage:");
55
+ expect(help).toContain("Examples:");
56
+ });
57
+
58
+ it("with unknown topic returns an error message", () => {
59
+ const help = formatHelp("nonexistent_command");
60
+ expect(help).toContain("Unknown help topic");
61
+ });
62
+ });
63
+
64
+ describe("formatNextSteps", () => {
65
+ it("returns next-step hints for each key command", () => {
66
+ const session = makeSession();
67
+ for (const cmd of ["new", "ls", "cd", "select", "set", "var", "show", "check"]) {
68
+ const output = formatNextSteps({ sessionId: session.id, command: cmd, session });
69
+ expect(output).toContain("Next steps:");
70
+ expect(output).toContain("graphiti query");
71
+ }
72
+ });
73
+ });
74
+ });