@salesforce/afv-skills 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/package.json +1 -1
  2. package/skills/activating-datacloud/SKILL.md +0 -1
  3. package/skills/analyzing-omnistudio-dependencies/SKILL.md +0 -1
  4. package/skills/applying-slds/SKILL.md +322 -0
  5. package/skills/applying-slds/checklists.md +83 -0
  6. package/skills/applying-slds/examples.md +283 -0
  7. package/skills/applying-slds/guidance/README.md +83 -0
  8. package/skills/applying-slds/guidance/blueprints-index.md +213 -0
  9. package/skills/applying-slds/guidance/icons-guidance.md +186 -0
  10. package/skills/applying-slds/guidance/overviews/borders.md +236 -0
  11. package/skills/applying-slds/guidance/overviews/color.md +266 -0
  12. package/skills/applying-slds/guidance/overviews/display-density.md +366 -0
  13. package/skills/applying-slds/guidance/overviews/icons.md +240 -0
  14. package/skills/applying-slds/guidance/overviews/illustrations.md +235 -0
  15. package/skills/applying-slds/guidance/overviews/shadows.md +176 -0
  16. package/skills/applying-slds/guidance/overviews/spacing.md +216 -0
  17. package/skills/applying-slds/guidance/overviews/typography.md +323 -0
  18. package/skills/applying-slds/guidance/overviews/utilities.md +542 -0
  19. package/skills/applying-slds/guidance/slds-development-guide.md +288 -0
  20. package/skills/applying-slds/guidance/styling-hooks/borders.md +202 -0
  21. package/skills/applying-slds/guidance/styling-hooks/color/expressive-palette-hooks.md +153 -0
  22. package/skills/applying-slds/guidance/styling-hooks/color/index.md +171 -0
  23. package/skills/applying-slds/guidance/styling-hooks/color/semantic/accent-hooks.md +204 -0
  24. package/skills/applying-slds/guidance/styling-hooks/color/semantic/feedback-hooks.md +768 -0
  25. package/skills/applying-slds/guidance/styling-hooks/color/semantic/surface-hooks.md +337 -0
  26. package/skills/applying-slds/guidance/styling-hooks/color/system-hooks.md +132 -0
  27. package/skills/applying-slds/guidance/styling-hooks/index.md +327 -0
  28. package/skills/applying-slds/guidance/styling-hooks/shadows.md +238 -0
  29. package/skills/applying-slds/guidance/styling-hooks/spacing.md +254 -0
  30. package/skills/applying-slds/guidance/styling-hooks/typography.md +448 -0
  31. package/skills/applying-slds/guidance/utilities/alignment.md +119 -0
  32. package/skills/applying-slds/guidance/utilities/borders.md +131 -0
  33. package/skills/applying-slds/guidance/utilities/box.md +125 -0
  34. package/skills/applying-slds/guidance/utilities/color.md +165 -0
  35. package/skills/applying-slds/guidance/utilities/dark-mode.md +111 -0
  36. package/skills/applying-slds/guidance/utilities/description-list.md +168 -0
  37. package/skills/applying-slds/guidance/utilities/floats.md +117 -0
  38. package/skills/applying-slds/guidance/utilities/grid.md +264 -0
  39. package/skills/applying-slds/guidance/utilities/horizontal-list.md +110 -0
  40. package/skills/applying-slds/guidance/utilities/hyphenation.md +84 -0
  41. package/skills/applying-slds/guidance/utilities/index.md +205 -0
  42. package/skills/applying-slds/guidance/utilities/interactions.md +89 -0
  43. package/skills/applying-slds/guidance/utilities/layout.md +109 -0
  44. package/skills/applying-slds/guidance/utilities/line-clamp.md +131 -0
  45. package/skills/applying-slds/guidance/utilities/margin.md +155 -0
  46. package/skills/applying-slds/guidance/utilities/media-object.md +161 -0
  47. package/skills/applying-slds/guidance/utilities/name-value-list.md +152 -0
  48. package/skills/applying-slds/guidance/utilities/padding.md +155 -0
  49. package/skills/applying-slds/guidance/utilities/position.md +177 -0
  50. package/skills/applying-slds/guidance/utilities/print.md +114 -0
  51. package/skills/applying-slds/guidance/utilities/scrollable.md +126 -0
  52. package/skills/applying-slds/guidance/utilities/sizing.md +190 -0
  53. package/skills/applying-slds/guidance/utilities/themes.md +121 -0
  54. package/skills/applying-slds/guidance/utilities/truncate.md +127 -0
  55. package/skills/applying-slds/guidance/utilities/typography.md +166 -0
  56. package/skills/applying-slds/guidance/utilities/vertical-list.md +166 -0
  57. package/skills/applying-slds/guidance/utilities/visibility.md +228 -0
  58. package/skills/applying-slds/metadata/README.md +84 -0
  59. package/skills/applying-slds/metadata/blueprints/components/accordion.yaml +304 -0
  60. package/skills/applying-slds/metadata/blueprints/components/activity-timeline.yaml +92 -0
  61. package/skills/applying-slds/metadata/blueprints/components/alert.yaml +103 -0
  62. package/skills/applying-slds/metadata/blueprints/components/app-launcher.yaml +94 -0
  63. package/skills/applying-slds/metadata/blueprints/components/avatar-group.yaml +81 -0
  64. package/skills/applying-slds/metadata/blueprints/components/avatar.yaml +97 -0
  65. package/skills/applying-slds/metadata/blueprints/components/badges.yaml +102 -0
  66. package/skills/applying-slds/metadata/blueprints/components/brand-band.yaml +198 -0
  67. package/skills/applying-slds/metadata/blueprints/components/breadcrumbs.yaml +95 -0
  68. package/skills/applying-slds/metadata/blueprints/components/builder-header.yaml +192 -0
  69. package/skills/applying-slds/metadata/blueprints/components/button-groups.yaml +82 -0
  70. package/skills/applying-slds/metadata/blueprints/components/button-icons.yaml +295 -0
  71. package/skills/applying-slds/metadata/blueprints/components/buttons.yaml +230 -0
  72. package/skills/applying-slds/metadata/blueprints/components/cards.yaml +124 -0
  73. package/skills/applying-slds/metadata/blueprints/components/carousel.yaml +140 -0
  74. package/skills/applying-slds/metadata/blueprints/components/chat.yaml +179 -0
  75. package/skills/applying-slds/metadata/blueprints/components/checkbox-button-group.yaml +192 -0
  76. package/skills/applying-slds/metadata/blueprints/components/checkbox-button.yaml +204 -0
  77. package/skills/applying-slds/metadata/blueprints/components/checkbox-toggle.yaml +177 -0
  78. package/skills/applying-slds/metadata/blueprints/components/checkbox.yaml +108 -0
  79. package/skills/applying-slds/metadata/blueprints/components/color-picker.yaml +172 -0
  80. package/skills/applying-slds/metadata/blueprints/components/combobox.yaml +136 -0
  81. package/skills/applying-slds/metadata/blueprints/components/counter.yaml +147 -0
  82. package/skills/applying-slds/metadata/blueprints/components/data-tables.yaml +157 -0
  83. package/skills/applying-slds/metadata/blueprints/components/datepickers.yaml +130 -0
  84. package/skills/applying-slds/metadata/blueprints/components/datetime-picker.yaml +155 -0
  85. package/skills/applying-slds/metadata/blueprints/components/docked-composer.yaml +201 -0
  86. package/skills/applying-slds/metadata/blueprints/components/docked-form-footer.yaml +161 -0
  87. package/skills/applying-slds/metadata/blueprints/components/docked-utility-bar.yaml +175 -0
  88. package/skills/applying-slds/metadata/blueprints/components/drop-zone.yaml +115 -0
  89. package/skills/applying-slds/metadata/blueprints/components/dueling-picklist.yaml +196 -0
  90. package/skills/applying-slds/metadata/blueprints/components/dynamic-icons.yaml +128 -0
  91. package/skills/applying-slds/metadata/blueprints/components/dynamic-menu.yaml +141 -0
  92. package/skills/applying-slds/metadata/blueprints/components/expandable-section.yaml +115 -0
  93. package/skills/applying-slds/metadata/blueprints/components/expression.yaml +143 -0
  94. package/skills/applying-slds/metadata/blueprints/components/feeds.yaml +125 -0
  95. package/skills/applying-slds/metadata/blueprints/components/file-selector.yaml +154 -0
  96. package/skills/applying-slds/metadata/blueprints/components/files.yaml +119 -0
  97. package/skills/applying-slds/metadata/blueprints/components/form-element.yaml +145 -0
  98. package/skills/applying-slds/metadata/blueprints/components/global-header.yaml +120 -0
  99. package/skills/applying-slds/metadata/blueprints/components/global-navigation.yaml +100 -0
  100. package/skills/applying-slds/metadata/blueprints/components/icons.yaml +138 -0
  101. package/skills/applying-slds/metadata/blueprints/components/illustration.yaml +205 -0
  102. package/skills/applying-slds/metadata/blueprints/components/input.yaml +151 -0
  103. package/skills/applying-slds/metadata/blueprints/components/list-builder.yaml +127 -0
  104. package/skills/applying-slds/metadata/blueprints/components/lookups.yaml +132 -0
  105. package/skills/applying-slds/metadata/blueprints/components/map.yaml +118 -0
  106. package/skills/applying-slds/metadata/blueprints/components/menus.yaml +134 -0
  107. package/skills/applying-slds/metadata/blueprints/components/modals.yaml +152 -0
  108. package/skills/applying-slds/metadata/blueprints/components/notifications.yaml +88 -0
  109. package/skills/applying-slds/metadata/blueprints/components/page-headers.yaml +135 -0
  110. package/skills/applying-slds/metadata/blueprints/components/panels.yaml +149 -0
  111. package/skills/applying-slds/metadata/blueprints/components/path.yaml +154 -0
  112. package/skills/applying-slds/metadata/blueprints/components/picklist.yaml +125 -0
  113. package/skills/applying-slds/metadata/blueprints/components/pills.yaml +154 -0
  114. package/skills/applying-slds/metadata/blueprints/components/popovers.yaml +120 -0
  115. package/skills/applying-slds/metadata/blueprints/components/progress-bar.yaml +110 -0
  116. package/skills/applying-slds/metadata/blueprints/components/progress-indicator.yaml +133 -0
  117. package/skills/applying-slds/metadata/blueprints/components/progress-ring.yaml +102 -0
  118. package/skills/applying-slds/metadata/blueprints/components/prompt.yaml +126 -0
  119. package/skills/applying-slds/metadata/blueprints/components/publishers.yaml +178 -0
  120. package/skills/applying-slds/metadata/blueprints/components/radio-button-group.yaml +172 -0
  121. package/skills/applying-slds/metadata/blueprints/components/radio-group.yaml +112 -0
  122. package/skills/applying-slds/metadata/blueprints/components/rich-text-editor.yaml +135 -0
  123. package/skills/applying-slds/metadata/blueprints/components/scoped-notifications.yaml +188 -0
  124. package/skills/applying-slds/metadata/blueprints/components/scoped-tabs.yaml +97 -0
  125. package/skills/applying-slds/metadata/blueprints/components/select.yaml +127 -0
  126. package/skills/applying-slds/metadata/blueprints/components/setup-assistant.yaml +152 -0
  127. package/skills/applying-slds/metadata/blueprints/components/slider.yaml +111 -0
  128. package/skills/applying-slds/metadata/blueprints/components/spinners.yaml +135 -0
  129. package/skills/applying-slds/metadata/blueprints/components/split-view.yaml +112 -0
  130. package/skills/applying-slds/metadata/blueprints/components/summary-detail.yaml +103 -0
  131. package/skills/applying-slds/metadata/blueprints/components/tabs.yaml +138 -0
  132. package/skills/applying-slds/metadata/blueprints/components/textarea.yaml +116 -0
  133. package/skills/applying-slds/metadata/blueprints/components/tiles.yaml +108 -0
  134. package/skills/applying-slds/metadata/blueprints/components/timepicker.yaml +111 -0
  135. package/skills/applying-slds/metadata/blueprints/components/toast.yaml +154 -0
  136. package/skills/applying-slds/metadata/blueprints/components/tooltips.yaml +107 -0
  137. package/skills/applying-slds/metadata/blueprints/components/tree-grid.yaml +116 -0
  138. package/skills/applying-slds/metadata/blueprints/components/trees.yaml +116 -0
  139. package/skills/applying-slds/metadata/blueprints/components/trial-bar.yaml +112 -0
  140. package/skills/applying-slds/metadata/blueprints/components/vertical-navigation.yaml +130 -0
  141. package/skills/applying-slds/metadata/blueprints/components/vertical-tabs.yaml +140 -0
  142. package/skills/applying-slds/metadata/blueprints/components/visual-picker.yaml +150 -0
  143. package/skills/applying-slds/metadata/blueprints/components/welcome-mat.yaml +136 -0
  144. package/skills/applying-slds/metadata/hooks-index.json +6272 -0
  145. package/skills/applying-slds/metadata/icon-metadata.json +38466 -0
  146. package/skills/applying-slds/metadata/utilities-index.json +21912 -0
  147. package/skills/applying-slds/references/component-selection.md +112 -0
  148. package/skills/applying-slds/references/icons-decision-guide.md +124 -0
  149. package/skills/applying-slds/references/styling-decision-guide.md +228 -0
  150. package/skills/applying-slds/references/utilities-quick-ref.md +125 -0
  151. package/skills/applying-slds/scripts/search-blueprints.cjs +117 -0
  152. package/skills/applying-slds/scripts/search-hooks.cjs +139 -0
  153. package/skills/applying-slds/scripts/search-icons.cjs +174 -0
  154. package/skills/applying-slds/scripts/search-utilities.cjs +161 -0
  155. package/skills/building-mobile-apps/SKILL.md +0 -1
  156. package/skills/building-omnistudio-callable-apex/SKILL.md +0 -1
  157. package/skills/building-omnistudio-datamapper/SKILL.md +0 -1
  158. package/skills/building-omnistudio-flexcard/SKILL.md +0 -1
  159. package/skills/building-omnistudio-integration-procedure/SKILL.md +0 -1
  160. package/skills/building-omnistudio-omniscript/SKILL.md +0 -1
  161. package/skills/building-sf-integrations/SKILL.md +0 -1
  162. package/skills/configuring-connected-apps/SKILL.md +0 -1
  163. package/skills/connecting-datacloud/SKILL.md +0 -1
  164. package/skills/creating-b2b-commerce-store/SKILL.md +0 -1
  165. package/skills/debugging-apex-logs/SKILL.md +0 -1
  166. package/skills/deploying-metadata/SKILL.md +0 -1
  167. package/skills/deploying-omnistudio-datapacks/SKILL.md +0 -1
  168. package/skills/developing-agentforce/SKILL.md +0 -1
  169. package/skills/fetching-salesforce-docs/SKILL.md +0 -1
  170. package/skills/generating-custom-lightning-type/SKILL.md +17 -39
  171. package/skills/generating-custom-lightning-type/assets/primitive-types-and-constraints.md +41 -0
  172. package/skills/generating-custom-lightning-type/references/widget-rendition.md +124 -0
  173. package/skills/generating-lwc-components/SKILL.md +0 -1
  174. package/skills/generating-mermaid-diagrams/SKILL.md +0 -1
  175. package/skills/generating-visual-diagrams/SKILL.md +0 -1
  176. package/skills/handling-sf-data/SKILL.md +0 -1
  177. package/skills/harmonizing-datacloud/SKILL.md +0 -1
  178. package/skills/integrating-b2b-commerce-open-code-components/SKILL.md +0 -1
  179. package/skills/investigating-agentforce-architecture/README.md +156 -0
  180. package/skills/investigating-agentforce-architecture/SKILL.md +230 -0
  181. package/skills/investigating-agentforce-architecture/assets/cli/describe_sobject.yaml +16 -0
  182. package/skills/investigating-agentforce-architecture/assets/cli/describe_tooling_sobject.yaml +17 -0
  183. package/skills/investigating-agentforce-architecture/assets/cli/list_metadata_genaiprompttemplate.yaml +17 -0
  184. package/skills/investigating-agentforce-architecture/assets/cli/org_display.yaml +15 -0
  185. package/skills/investigating-agentforce-architecture/assets/cli/retrieve_genai_plugin.yaml +18 -0
  186. package/skills/investigating-agentforce-architecture/assets/cli/show_access_token.yaml +27 -0
  187. package/skills/investigating-agentforce-architecture/assets/mermaid/action_tree.mmd +20 -0
  188. package/skills/investigating-agentforce-architecture/assets/mermaid/data_flow.mmd +19 -0
  189. package/skills/investigating-agentforce-architecture/assets/mermaid/dependency_graph.mmd +19 -0
  190. package/skills/investigating-agentforce-architecture/assets/mermaid/invocation_sequence.mmd +20 -0
  191. package/skills/investigating-agentforce-architecture/assets/mermaid/planner_state.mmd +18 -0
  192. package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_ids.soql +3 -0
  193. package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_names.soql +3 -0
  194. package/skills/investigating-agentforce-architecture/assets/soql/bot_definition_details.soql +3 -0
  195. package/skills/investigating-agentforce-architecture/assets/soql/bot_version_lookup.soql +4 -0
  196. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_by_ids.soql +3 -0
  197. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_ids_by_names.soql +3 -0
  198. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_view_by_durable_ids.soql +4 -0
  199. package/skills/investigating-agentforce-architecture/assets/soql/flow_metadata_by_id.soql +3 -0
  200. package/skills/investigating-agentforce-architecture/assets/soql/functions_by_plugins.soql +5 -0
  201. package/skills/investigating-agentforce-architecture/assets/soql/planner_attrs_by_parent_ids.soql +3 -0
  202. package/skills/investigating-agentforce-architecture/assets/soql/planner_bundle_functions.soql +3 -0
  203. package/skills/investigating-agentforce-architecture/assets/soql/planner_definition_by_agent_chain.soql +3 -0
  204. package/skills/investigating-agentforce-architecture/assets/soql/plugin_functions_by_plugin_ids.soql +3 -0
  205. package/skills/investigating-agentforce-architecture/assets/soql/plugin_instructions_by_plugin_ids.soql +3 -0
  206. package/skills/investigating-agentforce-architecture/assets/soql/plugins_by_planner.soql +4 -0
  207. package/skills/investigating-agentforce-architecture/references/architecture_sections.md +243 -0
  208. package/skills/investigating-agentforce-architecture/references/contract.json +244 -0
  209. package/skills/investigating-agentforce-architecture/references/soql_fields.md +512 -0
  210. package/skills/investigating-agentforce-architecture/scripts/_shared/__init__.py +1 -0
  211. package/skills/investigating-agentforce-architecture/scripts/_shared/fs_guard.py +329 -0
  212. package/skills/investigating-agentforce-architecture/scripts/_shared/paths.py +110 -0
  213. package/skills/investigating-agentforce-architecture/scripts/_shared/runtime.py +59 -0
  214. package/skills/investigating-agentforce-architecture/scripts/_shared/sql.py +10 -0
  215. package/skills/investigating-agentforce-architecture/scripts/cache_check.py +234 -0
  216. package/skills/investigating-agentforce-architecture/scripts/config.py +131 -0
  217. package/skills/investigating-agentforce-architecture/scripts/fetch_soql.py +689 -0
  218. package/skills/investigating-agentforce-architecture/scripts/finalize.py +295 -0
  219. package/skills/investigating-agentforce-architecture/scripts/main.py +2835 -0
  220. package/skills/investigating-agentforce-architecture/scripts/metadata_listing.py +265 -0
  221. package/skills/investigating-agentforce-architecture/scripts/parallel_retrieve.py +69 -0
  222. package/skills/investigating-agentforce-architecture/scripts/parse_bundle.py +215 -0
  223. package/skills/investigating-agentforce-architecture/scripts/parse_wave.py +845 -0
  224. package/skills/investigating-agentforce-architecture/scripts/probe_channels.py +302 -0
  225. package/skills/investigating-agentforce-architecture/scripts/render_architecture.py +1043 -0
  226. package/skills/investigating-agentforce-architecture/scripts/resolve_bot.py +255 -0
  227. package/skills/investigating-agentforce-architecture/scripts/resolve_invocation_target.py +130 -0
  228. package/skills/investigating-agentforce-architecture/scripts/rest_client.py +763 -0
  229. package/skills/investigating-agentforce-architecture/scripts/retrieve_planner.py +13 -0
  230. package/skills/investigating-agentforce-architecture/scripts/sf_cli.py +242 -0
  231. package/skills/investigating-agentforce-architecture/scripts/soql_loader.py +253 -0
  232. package/skills/investigating-agentforce-architecture/scripts/summarize_tree.py +143 -0
  233. package/skills/investigating-agentforce-architecture/scripts/tests/__init__.py +0 -0
  234. package/skills/investigating-agentforce-architecture/scripts/tests/_bootstrap.py +23 -0
  235. package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/__init__.py +0 -0
  236. package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/genai_payloads.py +400 -0
  237. package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check.py +307 -0
  238. package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check_main.py +283 -0
  239. package/skills/investigating-agentforce-architecture/scripts/tests/test_config.py +115 -0
  240. package/skills/investigating-agentforce-architecture/scripts/tests/test_end_to_end_fixture.py +651 -0
  241. package/skills/investigating-agentforce-architecture/scripts/tests/test_finalize.py +278 -0
  242. package/skills/investigating-agentforce-architecture/scripts/tests/test_flow_children_inflation.py +582 -0
  243. package/skills/investigating-agentforce-architecture/scripts/tests/test_fs_guard.py +113 -0
  244. package/skills/investigating-agentforce-architecture/scripts/tests/test_iterative_wave_b.py +478 -0
  245. package/skills/investigating-agentforce-architecture/scripts/tests/test_main_pipeline.py +3359 -0
  246. package/skills/investigating-agentforce-architecture/scripts/tests/test_parallel_retrieve.py +131 -0
  247. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_bundle.py +400 -0
  248. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave.py +644 -0
  249. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_classifiers.py +224 -0
  250. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_helpers.py +380 -0
  251. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_main.py +397 -0
  252. package/skills/investigating-agentforce-architecture/scripts/tests/test_per_branch_visited.py +244 -0
  253. package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_channels.py +359 -0
  254. package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_cli_recipes.py +185 -0
  255. package/skills/investigating-agentforce-architecture/scripts/tests/test_render_architecture.py +810 -0
  256. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_bot.py +203 -0
  257. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_creds.py +157 -0
  258. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_invocation_target.py +145 -0
  259. package/skills/investigating-agentforce-architecture/scripts/tests/test_rest_client.py +1253 -0
  260. package/skills/investigating-agentforce-architecture/scripts/tests/test_runtime_override.py +100 -0
  261. package/skills/investigating-agentforce-architecture/scripts/tests/test_sf_cli.py +261 -0
  262. package/skills/investigating-agentforce-architecture/scripts/tests/test_signature_stamping.py +466 -0
  263. package/skills/investigating-agentforce-architecture/scripts/tests/test_soql_loader.py +501 -0
  264. package/skills/investigating-agentforce-architecture/scripts/tests/test_summarize_tree.py +241 -0
  265. package/skills/investigating-agentforce-architecture/scripts/tests/test_write_emit_ctx.py +480 -0
  266. package/skills/investigating-agentforce-architecture/tools/emit_env.py +157 -0
  267. package/skills/investigating-agentforce-architecture/tools/emit_result.py +262 -0
  268. package/skills/investigating-agentforce-architecture/tools/sanitize.py +33 -0
  269. package/skills/investigating-agentforce-architecture/tools/write_emit_ctx.py +332 -0
  270. package/skills/investigating-agentforce-d360/README.md +123 -0
  271. package/skills/investigating-agentforce-d360/SKILL.md +163 -0
  272. package/skills/investigating-agentforce-d360/assets/dc/app_generation.sql +51 -0
  273. package/skills/investigating-agentforce-d360/assets/dc/content_category.sql +44 -0
  274. package/skills/investigating-agentforce-d360/assets/dc/content_quality.sql +41 -0
  275. package/skills/investigating-agentforce-d360/assets/dc/discover_sessions.sql +36 -0
  276. package/skills/investigating-agentforce-d360/assets/dc/feedback.sql +47 -0
  277. package/skills/investigating-agentforce-d360/assets/dc/feedback_details.sql +38 -0
  278. package/skills/investigating-agentforce-d360/assets/dc/gateway_records.sql +45 -0
  279. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_llm.sql +50 -0
  280. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_metadata.sql +44 -0
  281. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_tags.sql +42 -0
  282. package/skills/investigating-agentforce-d360/assets/dc/gateway_requests.sql +89 -0
  283. package/skills/investigating-agentforce-d360/assets/dc/gateway_responses.sql +43 -0
  284. package/skills/investigating-agentforce-d360/assets/dc/generations.sql +52 -0
  285. package/skills/investigating-agentforce-d360/assets/dc/interactions.sql +53 -0
  286. package/skills/investigating-agentforce-d360/assets/dc/messages.sql +53 -0
  287. package/skills/investigating-agentforce-d360/assets/dc/messaging_session.sql +37 -0
  288. package/skills/investigating-agentforce-d360/assets/dc/moment_interactions.sql +34 -0
  289. package/skills/investigating-agentforce-d360/assets/dc/moments.sql +39 -0
  290. package/skills/investigating-agentforce-d360/assets/dc/participants.sql +48 -0
  291. package/skills/investigating-agentforce-d360/assets/dc/sessions.sql +78 -0
  292. package/skills/investigating-agentforce-d360/assets/dc/steps.sql +64 -0
  293. package/skills/investigating-agentforce-d360/assets/dc/tag_associations.sql +46 -0
  294. package/skills/investigating-agentforce-d360/assets/dc/tag_definition_associations.sql +37 -0
  295. package/skills/investigating-agentforce-d360/assets/dc/tag_definitions.sql +50 -0
  296. package/skills/investigating-agentforce-d360/assets/dc/tags.sql +37 -0
  297. package/skills/investigating-agentforce-d360/assets/dc/telemetry_spans.sql +55 -0
  298. package/skills/investigating-agentforce-d360/references/artifacts.md +50 -0
  299. package/skills/investigating-agentforce-d360/references/dc_dmo_fields.md +823 -0
  300. package/skills/investigating-agentforce-d360/references/dc_pipeline_contract.md +608 -0
  301. package/skills/investigating-agentforce-d360/scripts/_shared/__init__.py +2 -0
  302. package/skills/investigating-agentforce-d360/scripts/_shared/cli_override.py +98 -0
  303. package/skills/investigating-agentforce-d360/scripts/_shared/fs_guard.py +334 -0
  304. package/skills/investigating-agentforce-d360/scripts/_shared/paths.py +155 -0
  305. package/skills/investigating-agentforce-d360/scripts/_shared/runtime.py +59 -0
  306. package/skills/investigating-agentforce-d360/scripts/_shared/sql.py +14 -0
  307. package/skills/investigating-agentforce-d360/scripts/assemble_dc.py +1624 -0
  308. package/skills/investigating-agentforce-d360/scripts/config.py +45 -0
  309. package/skills/investigating-agentforce-d360/scripts/dc.py +188 -0
  310. package/skills/investigating-agentforce-d360/scripts/discover_sessions.py +556 -0
  311. package/skills/investigating-agentforce-d360/scripts/fetch_dc.py +1045 -0
  312. package/skills/investigating-agentforce-d360/scripts/render_dc.py +1750 -0
  313. package/skills/investigating-agentforce-d360/scripts/resolve_session.py +264 -0
  314. package/skills/investigating-agentforce-d360/scripts/storage.py +92 -0
  315. package/skills/investigating-agentforce-d360/scripts/tests/__init__.py +0 -0
  316. package/skills/investigating-agentforce-d360/scripts/tests/_bootstrap.py +15 -0
  317. package/skills/investigating-agentforce-d360/scripts/tests/fixtures/__init__.py +0 -0
  318. package/skills/investigating-agentforce-d360/scripts/tests/fixtures/synthetic_session.py +424 -0
  319. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_bootstrap_and_mode.py +115 -0
  320. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct.py +220 -0
  321. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct_integration.py +158 -0
  322. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_helpers.py +287 -0
  323. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_integration.py +247 -0
  324. package/skills/investigating-agentforce-d360/scripts/tests/test_dc_and_resolve_session.py +433 -0
  325. package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions.py +458 -0
  326. package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions_grep_ci.py +193 -0
  327. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_helpers.py +266 -0
  328. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_identity.py +528 -0
  329. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_main.py +251 -0
  330. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall.py +229 -0
  331. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall_full.py +283 -0
  332. package/skills/investigating-agentforce-d360/scripts/tests/test_identity_coherence.py +327 -0
  333. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_branches.py +256 -0
  334. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_gateway_direct.py +130 -0
  335. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_helpers.py +291 -0
  336. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_integration.py +220 -0
  337. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_planner_llm_calls.py +284 -0
  338. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_show_prompts_gating.py +215 -0
  339. package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_from_disk.py +100 -0
  340. package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_session_main.py +149 -0
  341. package/skills/investigating-agentforce-d360/scripts/tests/test_runtime_override.py +104 -0
  342. package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape.py +95 -0
  343. package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape_dropped_by_stdm.py +85 -0
  344. package/skills/managing-managed-event-subscription/SKILL.md +152 -0
  345. package/skills/managing-managed-event-subscription/assets/managed-event-subscription-template.xml +20 -0
  346. package/skills/managing-managed-event-subscription/references/delete-guide.md +57 -0
  347. package/skills/managing-managed-event-subscription/references/topic-name-formats.md +26 -0
  348. package/skills/managing-managed-event-subscription/references/update-constraints.md +30 -0
  349. package/skills/modeling-omnistudio-epc-catalog/SKILL.md +0 -1
  350. package/skills/observing-agentforce/SKILL.md +0 -1
  351. package/skills/orchestrating-datacloud/SKILL.md +0 -1
  352. package/skills/preparing-datacloud/SKILL.md +0 -1
  353. package/skills/querying-soql/SKILL.md +0 -1
  354. package/skills/retrieving-datacloud/SKILL.md +0 -1
  355. package/skills/running-apex-tests/SKILL.md +0 -1
  356. package/skills/running-code-analyzer/SKILL.md +0 -1
  357. package/skills/segmenting-datacloud/SKILL.md +0 -1
  358. package/skills/testing-agentforce/SKILL.md +0 -1
  359. package/skills/uplifting-components-to-slds2/SKILL.md +3 -2
  360. package/skills/uplifting-components-to-slds2/references/color-hooks-decision-guide.md +30 -9
  361. package/skills/uplifting-components-to-slds2/references/examples.md +24 -6
  362. package/skills/validating-slds/SKILL.md +262 -0
  363. package/skills/validating-slds/references/quality-checks.md +308 -0
  364. package/skills/validating-slds/references/report-format.md +302 -0
  365. package/skills/validating-slds/scripts/analyze-quality.cjs +521 -0
@@ -0,0 +1,501 @@
1
+ """Tests for load_soql revalidates every substituted string."""
2
+ from __future__ import annotations
3
+
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest import mock
8
+
9
+ from . import _bootstrap # noqa: F401 — sys.path setup
10
+
11
+
12
+ class LoadSoqlValidationTests(unittest.TestCase):
13
+ """every param must be revalidated at the substitution boundary."""
14
+
15
+ def setUp(self) -> None:
16
+ # Use a throwaway SOQL_DIR populated per test so we don't depend on
17
+ # the shipped assets having a specific template name.
18
+ self._tmpdir = tempfile.TemporaryDirectory()
19
+ self.soql_dir = Path(self._tmpdir.name)
20
+ # Patch config.SOQL_DIR BEFORE importing soql_loader so the module
21
+ # constant reflects the tmpdir. Because soql_loader binds SOQL_DIR
22
+ # at import, we re-import it fresh under the patch.
23
+ self._patch = mock.patch("config.SOQL_DIR", self.soql_dir)
24
+ self._patch.start()
25
+ # Force a fresh import so load_soql reads the patched SOQL_DIR.
26
+ import importlib
27
+ import soql_loader # type: ignore
28
+ importlib.reload(soql_loader)
29
+ self.soql_loader = soql_loader
30
+
31
+ def tearDown(self) -> None:
32
+ self._patch.stop()
33
+ self._tmpdir.cleanup()
34
+
35
+ def _write_template(self, name: str, body: str) -> None:
36
+ (self.soql_dir / f"{name}.soql").write_text(body)
37
+
38
+ # ---- happy path -------------------------------------------------------
39
+
40
+ def test_valid_param_substitutes(self):
41
+ self._write_template(
42
+ "bot_lookup",
43
+ "SELECT Id FROM BotDefinition WHERE DeveloperName = '{{NAME}}'",
44
+ )
45
+ out = self.soql_loader.load_soql("bot_lookup", NAME="MyAgent")
46
+ self.assertEqual(
47
+ out,
48
+ "SELECT Id FROM BotDefinition WHERE DeveloperName = 'MyAgent'",
49
+ )
50
+
51
+ def test_multiple_valid_params(self):
52
+ self._write_template(
53
+ "lookup",
54
+ "SELECT Id FROM Obj WHERE A='{{A}}' AND B='{{B}}'",
55
+ )
56
+ out = self.soql_loader.load_soql("lookup", A="Foo", B="Bar_v2")
57
+ self.assertIn("A='Foo'", out)
58
+ self.assertIn("B='Bar_v2'", out)
59
+
60
+ # ---- injection attempts must raise ------------------------------------
61
+
62
+ def test_injection_quote_or_clause_raises(self):
63
+ self._write_template("q", "SELECT Id FROM X WHERE Name='{{NAME}}'")
64
+ with self.assertRaises(self.soql_loader.SoqlParamError) as ctx:
65
+ self.soql_loader.load_soql("q", NAME="x' OR Id!=null--")
66
+ self.assertEqual(ctx.exception.key, "NAME")
67
+
68
+ def test_injection_drop_table_raises(self):
69
+ self._write_template("q", "SELECT Id FROM X WHERE Name='{{NAME}}'")
70
+ with self.assertRaises(self.soql_loader.SoqlParamError):
71
+ self.soql_loader.load_soql("q", NAME="'; DROP TABLE x;--")
72
+
73
+ def test_injection_or_1eq1_raises(self):
74
+ self._write_template("q", "SELECT Id FROM X WHERE Name='{{NAME}}'")
75
+ with self.assertRaises(self.soql_loader.SoqlParamError):
76
+ self.soql_loader.load_soql("q", NAME="x OR 1=1")
77
+
78
+ def test_whitespace_rejected(self):
79
+ self._write_template("q", "SELECT Id FROM X WHERE Name='{{NAME}}'")
80
+ with self.assertRaises(self.soql_loader.SoqlParamError):
81
+ self.soql_loader.load_soql("q", NAME="x OR y") # space
82
+
83
+ def test_dash_rejected(self):
84
+ # Common SOQL-injection payload prefix.
85
+ self._write_template("q", "SELECT Id FROM X WHERE Name='{{NAME}}'")
86
+ with self.assertRaises(self.soql_loader.SoqlParamError):
87
+ self.soql_loader.load_soql("q", NAME="x-y")
88
+
89
+ # ---- type errors ------------------------------------------------------
90
+
91
+ def test_non_string_value_raises(self):
92
+ self._write_template("q", "SELECT {{N}}")
93
+ with self.assertRaises(self.soql_loader.SoqlParamError):
94
+ self.soql_loader.load_soql("q", N=42)
95
+
96
+ def test_none_value_raises(self):
97
+ self._write_template("q", "SELECT {{N}}")
98
+ with self.assertRaises(self.soql_loader.SoqlParamError):
99
+ self.soql_loader.load_soql("q", N=None)
100
+
101
+ def test_empty_string_raises(self):
102
+ self._write_template("q", "SELECT {{N}}")
103
+ with self.assertRaises(self.soql_loader.SoqlParamError):
104
+ self.soql_loader.load_soql("q", N="")
105
+
106
+ # ---- single-pass substitution guarantee -------------------------------
107
+
108
+ def test_value_containing_other_placeholder_does_not_retrigger(self):
109
+ """A valid value that contains `{{OTHER}}` must NOT trigger a
110
+ second substitution pass on OTHER. str.replace is single-pass
111
+ by contract; we assert that the validator's regex prevents any
112
+ value from containing `{`, `}`, or whitespace in the first place
113
+ — so the injection path is closed at validation, not at
114
+ substitution.
115
+ """
116
+ self._write_template("q", "SELECT {{A}} AND {{B}}")
117
+ # Reject values that carry placeholder syntax — the regex forbids
118
+ # `{` and `}` (they're not in [A-Za-z0-9_]).
119
+ with self.assertRaises(self.soql_loader.SoqlParamError):
120
+ self.soql_loader.load_soql("q", A="{{B}}", B="safe")
121
+
122
+ def test_raw_replace_is_single_pass(self):
123
+ """Belt-and-braces: even if a value slipped through (it cannot,
124
+ per the regex), Python's str.replace does not recursively scan
125
+ the output. Simulate by bypassing the validator and confirming
126
+ str.replace behavior directly.
127
+ """
128
+ template = "SELECT {{A}} AND {{B}}"
129
+ # Substitute A first with a value that "looks like" the B
130
+ # placeholder. str.replace for B should then replace the
131
+ # template's own `{{B}}` — but A's embedded `{{B}}` should stay
132
+ # (Python replaces left-to-right in a single pass; the output
133
+ # of the A replacement is the NEW string and B's pass runs on
134
+ # that new string). The assertion below therefore confirms
135
+ # that B's replacement hits BOTH occurrences (the original and
136
+ # the one inside A's substituted value) — demonstrating that
137
+ # str.replace is NOT recursive on the ORIGINAL template alone,
138
+ # but IS a single linear scan of the full post-A string.
139
+ step1 = template.replace("{{A}}", "{{B}}")
140
+ step2 = step1.replace("{{B}}", "replaced")
141
+ # Both occurrences of `{{B}}` in step1 are replaced in a single
142
+ # left-to-right pass. This is the property tested for.
143
+ self.assertEqual(step2, "SELECT replaced AND replaced")
144
+ # The safety net: if a caller EVER skipped revalidation and
145
+ # allowed a `{{X}}` to reach substitution, the above behavior
146
+ # could matter — which is PRECISELY why revalidates at
147
+ # the boundary. The regex denies `{`, `}`, whitespace, making
148
+ # this scenario unreachable in production.
149
+
150
+
151
+ class LoadSoqlNameValidationTests(unittest.TestCase):
152
+ """the `name` argument is validated before any filesystem access.
153
+
154
+ Without this, a caller that sources `name` from data (config file, user
155
+ argument, discovered string) could read arbitrary files via traversal
156
+ (`../../../etc/passwd`). The regex gate closes that before `read_text()`.
157
+ """
158
+
159
+ def setUp(self) -> None:
160
+ self._tmpdir = tempfile.TemporaryDirectory()
161
+ self.soql_dir = Path(self._tmpdir.name)
162
+ self._patch = mock.patch("config.SOQL_DIR", self.soql_dir)
163
+ self._patch.start()
164
+ import importlib
165
+ import soql_loader # type: ignore
166
+ importlib.reload(soql_loader)
167
+ self.soql_loader = soql_loader
168
+
169
+ def tearDown(self) -> None:
170
+ self._patch.stop()
171
+ self._tmpdir.cleanup()
172
+
173
+ # ---- traversal attempts must raise before any file read ---------------
174
+
175
+ def test_parent_traversal_raises(self):
176
+ """`../../../etc/passwd` must be caught by validation, not by the
177
+ filesystem. The error must be SoqlParamError (clearly labeled) —
178
+ NOT a bare FileNotFoundError or a ValidationError with raw path.
179
+ """
180
+ with self.assertRaises(self.soql_loader.SoqlParamError) as ctx:
181
+ self.soql_loader.load_soql("../../../etc/passwd")
182
+ self.assertEqual(ctx.exception.key, "soql_template_name")
183
+ # The absolute SOQL_DIR must NOT appear in the surfaced message.
184
+ self.assertNotIn(str(self.soql_dir), str(ctx.exception))
185
+
186
+ def test_dotdot_alone_raises(self):
187
+ with self.assertRaises(self.soql_loader.SoqlParamError):
188
+ self.soql_loader.load_soql("..")
189
+
190
+ def test_slash_in_name_raises(self):
191
+ """`/` is not in [A-Za-z0-9_], so `plugins/by_planner` must be
192
+ caught at validation — never reach the filesystem."""
193
+ with self.assertRaises(self.soql_loader.SoqlParamError):
194
+ self.soql_loader.load_soql("plugins/by_planner")
195
+
196
+ def test_backslash_in_name_raises(self):
197
+ """Windows-style separators — belt and braces."""
198
+ with self.assertRaises(self.soql_loader.SoqlParamError):
199
+ self.soql_loader.load_soql("plugins\\by_planner")
200
+
201
+ def test_absolute_path_raises(self):
202
+ """A caller passing an absolute path (a different traversal variant)
203
+ must still hit validation, not a false-negative file read."""
204
+ with self.assertRaises(self.soql_loader.SoqlParamError):
205
+ self.soql_loader.load_soql("/etc/passwd")
206
+
207
+ def test_empty_name_raises(self):
208
+ with self.assertRaises(self.soql_loader.SoqlParamError):
209
+ self.soql_loader.load_soql("")
210
+
211
+ def test_none_name_raises(self):
212
+ with self.assertRaises(self.soql_loader.SoqlParamError):
213
+ self.soql_loader.load_soql(None) # type: ignore[arg-type]
214
+
215
+ def test_whitespace_in_name_raises(self):
216
+ with self.assertRaises(self.soql_loader.SoqlParamError):
217
+ self.soql_loader.load_soql("bot lookup")
218
+
219
+ def test_traversal_never_reads_filesystem(self):
220
+ """Validation runs before any I/O — verify read_text is never
221
+ called when the name is invalid. If the order ever flips, a bad
222
+ name could still leak a FileNotFoundError with raw path info.
223
+ """
224
+ with mock.patch.object(Path, "read_text") as mock_read:
225
+ with self.assertRaises(self.soql_loader.SoqlParamError):
226
+ self.soql_loader.load_soql("../../../evil")
227
+ mock_read.assert_not_called()
228
+
229
+
230
+ class LoadSoqlTemplateNotFoundTests(unittest.TestCase):
231
+ """FileNotFoundError is translated into SoqlTemplateNotFound
232
+ whose message is free of filesystem-path leakage.
233
+ """
234
+
235
+ def setUp(self) -> None:
236
+ self._tmpdir = tempfile.TemporaryDirectory()
237
+ self.soql_dir = Path(self._tmpdir.name)
238
+ self._patch = mock.patch("config.SOQL_DIR", self.soql_dir)
239
+ self._patch.start()
240
+ import importlib
241
+ import soql_loader # type: ignore
242
+ importlib.reload(soql_loader)
243
+ self.soql_loader = soql_loader
244
+
245
+ def tearDown(self) -> None:
246
+ self._patch.stop()
247
+ self._tmpdir.cleanup()
248
+
249
+ def test_missing_template_raises_custom_exception(self):
250
+ with self.assertRaises(self.soql_loader.SoqlTemplateNotFound) as ctx:
251
+ self.soql_loader.load_soql("nonexistent_template")
252
+ self.assertEqual(ctx.exception.name, "nonexistent_template")
253
+
254
+ def test_missing_template_message_excludes_soql_dir(self):
255
+ """The SOQL_DIR absolute path must NOT appear in the surfaced error
256
+ string — information-disclosure hygiene. Attackers don't need to know
257
+ where the skill install lives on disk.
258
+ """
259
+ with self.assertRaises(self.soql_loader.SoqlTemplateNotFound) as ctx:
260
+ self.soql_loader.load_soql("nonexistent_template")
261
+ msg = str(ctx.exception)
262
+ self.assertNotIn(str(self.soql_dir), msg)
263
+ self.assertNotIn(".soql", msg)
264
+ # Template name IS allowed in the message — that's the triage signal.
265
+ self.assertIn("nonexistent_template", msg)
266
+
267
+ def test_missing_template_does_not_leak_via_cause_chain(self):
268
+ """`raise ... from None` is load-bearing: without it, the
269
+ FileNotFoundError (with its raw `filename` attribute) would be
270
+ reachable via `exception.__cause__`. Verify the chain is severed.
271
+ """
272
+ try:
273
+ self.soql_loader.load_soql("nonexistent_template")
274
+ except self.soql_loader.SoqlTemplateNotFound as e:
275
+ # `from None` sets __cause__ = None AND __suppress_context__ = True.
276
+ # Either alone suppresses traceback rendering of the underlying
277
+ # FileNotFoundError.
278
+ self.assertIsNone(e.__cause__)
279
+ self.assertTrue(e.__suppress_context__)
280
+ else:
281
+ self.fail("expected SoqlTemplateNotFound")
282
+
283
+ def test_not_found_exception_is_distinct_from_file_not_found(self):
284
+ """Callers should be able to tell 'template missing' apart from
285
+ 'permission denied / I/O error' at the except-clause layer.
286
+ """
287
+ self.assertFalse(
288
+ issubclass(
289
+ self.soql_loader.SoqlTemplateNotFound,
290
+ FileNotFoundError,
291
+ ),
292
+ "SoqlTemplateNotFound must not subclass FileNotFoundError",
293
+ )
294
+
295
+ def test_valid_name_with_params_unchanged(self):
296
+ """Regression: must not break the happy path."""
297
+ (self.soql_dir / "lookup.soql").write_text(
298
+ "SELECT Id FROM X WHERE Name = '{{NAME}}'"
299
+ )
300
+ out = self.soql_loader.load_soql("lookup", NAME="MyAgent")
301
+ self.assertEqual(
302
+ out,
303
+ "SELECT Id FROM X WHERE Name = 'MyAgent'",
304
+ )
305
+
306
+
307
+ class LoadSoqlInListParamTests(unittest.TestCase):
308
+ """`load_soql_in` renders `WHERE X IN (...)` list placeholders.
309
+
310
+ Same validation surface as `load_soql` — every list element passes
311
+ through `fs_guard.validate_api_name`. Empty lists fail fast (SOQL
312
+ `WHERE X IN ()` is invalid). Dedup + sort are load-bearing for
313
+ stable cache keys.
314
+ """
315
+
316
+ def setUp(self) -> None:
317
+ self._tmpdir = tempfile.TemporaryDirectory()
318
+ self.soql_dir = Path(self._tmpdir.name)
319
+ self._patch = mock.patch("config.SOQL_DIR", self.soql_dir)
320
+ self._patch.start()
321
+ import importlib
322
+ import soql_loader # type: ignore
323
+ importlib.reload(soql_loader)
324
+ self.soql_loader = soql_loader
325
+
326
+ def tearDown(self) -> None:
327
+ self._patch.stop()
328
+ self._tmpdir.cleanup()
329
+
330
+ def _write_template(self, name: str, body: str) -> None:
331
+ (self.soql_dir / f"{name}.soql").write_text(body)
332
+
333
+ # ---- happy path ------------------------------------------------------
334
+
335
+ def test_list_params_render_single_quoted_comma_joined(self):
336
+ self._write_template(
337
+ "apex_by_names",
338
+ "SELECT Id FROM ApexClass WHERE Name IN ({{NAMES_LIST}})",
339
+ )
340
+ out = self.soql_loader.load_soql_in(
341
+ "apex_by_names",
342
+ list_params={"NAMES_LIST": ["ClassA", "ClassB"]},
343
+ )
344
+ self.assertIn("WHERE Name IN ('ClassA','ClassB')", out)
345
+
346
+ def test_mixed_string_and_list_params(self):
347
+ self._write_template(
348
+ "functions_q",
349
+ "SELECT Id FROM GenAiFunctionDefinition "
350
+ "WHERE PlannerId = '{{PLANNER_ID}}' OR PluginId IN ({{PLUGIN_IDS}})",
351
+ )
352
+ out = self.soql_loader.load_soql_in(
353
+ "functions_q",
354
+ string_params={"PLANNER_ID": "X"},
355
+ list_params={"PLUGIN_IDS": ["P1", "P2"]},
356
+ )
357
+ self.assertIn("PlannerId = 'X'", out)
358
+ self.assertIn("PluginId IN ('P1','P2')", out)
359
+
360
+ def test_string_params_optional(self):
361
+ """string_params defaults to None — list_params alone should work."""
362
+ self._write_template(
363
+ "flow_by_names",
364
+ "SELECT Id FROM FlowDefinition WHERE DeveloperName IN ({{NAMES_LIST}})",
365
+ )
366
+ out = self.soql_loader.load_soql_in(
367
+ "flow_by_names",
368
+ list_params={"NAMES_LIST": ["Flow_A"]},
369
+ )
370
+ self.assertIn("IN ('Flow_A')", out)
371
+
372
+ # ---- validation: list elements must match api_name regex ---------------
373
+
374
+ def test_injection_in_list_element_raises_with_list_key(self):
375
+ """A SOQL injection attempt inside a list element must raise
376
+ SoqlParamError whose `key` is the LIST key (not the element
377
+ index or a synthetic name) — so the caller can log / mark
378
+ `_unresolved[NAMES_LIST]` at the upstream boundary.
379
+ """
380
+ self._write_template(
381
+ "q", "SELECT Id FROM ApexClass WHERE Name IN ({{NAMES_LIST}})",
382
+ )
383
+ with self.assertRaises(self.soql_loader.SoqlParamError) as ctx:
384
+ self.soql_loader.load_soql_in(
385
+ "q",
386
+ list_params={"NAMES_LIST": ["ClassA", "ClassB'; DROP TABLE x;--"]},
387
+ )
388
+ self.assertEqual(ctx.exception.key, "NAMES_LIST")
389
+
390
+ def test_non_string_element_raises(self):
391
+ self._write_template("q", "SELECT Id FROM X WHERE Id IN ({{IDS}})")
392
+ with self.assertRaises(self.soql_loader.SoqlParamError) as ctx:
393
+ self.soql_loader.load_soql_in(
394
+ "q", list_params={"IDS": ["ClassA", 42]},
395
+ )
396
+ self.assertEqual(ctx.exception.key, "IDS")
397
+
398
+ def test_whitespace_in_list_element_raises(self):
399
+ self._write_template("q", "SELECT Id FROM X WHERE Id IN ({{IDS}})")
400
+ with self.assertRaises(self.soql_loader.SoqlParamError):
401
+ self.soql_loader.load_soql_in(
402
+ "q", list_params={"IDS": ["ClassA", "x OR 1=1"]},
403
+ )
404
+
405
+ # ---- empty list fails fast --------------------------------------------
406
+
407
+ def test_empty_list_raises(self):
408
+ """SOQL `WHERE X IN ()` is a syntax error; fail at the loader,
409
+ not at the CLI. The reason string must mention empty so the
410
+ `_unresolved` bucket can be tagged distinctly from injection.
411
+ """
412
+ self._write_template("q", "SELECT Id FROM X WHERE Id IN ({{IDS}})")
413
+ with self.assertRaises(self.soql_loader.SoqlParamError) as ctx:
414
+ self.soql_loader.load_soql_in("q", list_params={"IDS": []})
415
+ self.assertEqual(ctx.exception.key, "IDS")
416
+ self.assertIn("empty", ctx.exception.reason.lower())
417
+
418
+ def test_list_params_not_a_list_raises(self):
419
+ """Defensive: a dict or string passed in `list_params[KEY]`
420
+ must be rejected — silently iterating a string would produce
421
+ one-char-per-element SOQL, which is worse than an explicit error.
422
+ """
423
+ self._write_template("q", "SELECT Id FROM X WHERE Id IN ({{IDS}})")
424
+ with self.assertRaises(self.soql_loader.SoqlParamError):
425
+ # type: ignore[arg-type]
426
+ self.soql_loader.load_soql_in("q", list_params={"IDS": "notalist"})
427
+
428
+ # ---- dedupe + deterministic order -------------------------------------
429
+
430
+ def test_dedupe_eliminates_duplicates(self):
431
+ self._write_template("q", "SELECT Id FROM X WHERE Name IN ({{NS}})")
432
+ out = self.soql_loader.load_soql_in(
433
+ "q", list_params={"NS": ["ClassA", "ClassA", "ClassB"]},
434
+ )
435
+ self.assertEqual(out.count("'ClassA'"), 1)
436
+ self.assertEqual(out.count("'ClassB'"), 1)
437
+
438
+ def test_output_is_sorted_for_deterministic_order(self):
439
+ """Stable cache-key requirement: input order MUST NOT affect
440
+ output. `sorted(set(...))` lands ClassA before ClassB regardless
441
+ of input order.
442
+ """
443
+ self._write_template("q", "SELECT Id FROM X WHERE Name IN ({{NS}})")
444
+ out1 = self.soql_loader.load_soql_in(
445
+ "q", list_params={"NS": ["ClassB", "ClassA"]},
446
+ )
447
+ out2 = self.soql_loader.load_soql_in(
448
+ "q", list_params={"NS": ["ClassA", "ClassB"]},
449
+ )
450
+ self.assertEqual(out1, out2)
451
+ # And the order is alphabetical, not input-dependent.
452
+ self.assertLess(out1.index("'ClassA'"), out1.index("'ClassB'"))
453
+
454
+ # ---- scalar validation path shared with load_soql ---------------------
455
+
456
+ def test_scalar_injection_still_raises(self):
457
+ """string_params go through the same validator as `load_soql` —
458
+ no shortcut. A SOQL-injection attempt in a scalar must still
459
+ surface SoqlParamError.
460
+ """
461
+ self._write_template(
462
+ "q",
463
+ "SELECT Id FROM X WHERE P = '{{PID}}' OR Q IN ({{LIST}})",
464
+ )
465
+ with self.assertRaises(self.soql_loader.SoqlParamError) as ctx:
466
+ self.soql_loader.load_soql_in(
467
+ "q",
468
+ string_params={"PID": "x' OR Id!=null--"},
469
+ list_params={"LIST": ["A"]},
470
+ )
471
+ self.assertEqual(ctx.exception.key, "PID")
472
+
473
+ # ---- template-name validation reused ----------------------------------
474
+
475
+ def test_template_traversal_raises(self):
476
+ with self.assertRaises(self.soql_loader.SoqlParamError) as ctx:
477
+ self.soql_loader.load_soql_in(
478
+ "../../../evil", list_params={"IDS": ["A"]},
479
+ )
480
+ self.assertEqual(ctx.exception.key, "soql_template_name")
481
+
482
+ def test_missing_template_raises_template_not_found(self):
483
+ with self.assertRaises(self.soql_loader.SoqlTemplateNotFound):
484
+ self.soql_loader.load_soql_in(
485
+ "nonexistent_for_in", list_params={"IDS": ["A"]},
486
+ )
487
+
488
+ # ---- existing load_soql unchanged -------------------------------------
489
+
490
+ def test_load_soql_signature_untouched(self):
491
+ """contract: `load_soql(name, **params)` keeps its original
492
+ signature — no kwargs-only, no extra params. A caller that still
493
+ uses the old form must keep working.
494
+ """
495
+ self._write_template("q", "SELECT Id FROM X WHERE Name = '{{NAME}}'")
496
+ out = self.soql_loader.load_soql("q", NAME="Foo")
497
+ self.assertIn("'Foo'", out)
498
+
499
+
500
+ if __name__ == "__main__":
501
+ unittest.main()