@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.
- package/package.json +1 -1
- package/skills/activating-datacloud/SKILL.md +0 -1
- package/skills/analyzing-omnistudio-dependencies/SKILL.md +0 -1
- package/skills/applying-slds/SKILL.md +322 -0
- package/skills/applying-slds/checklists.md +83 -0
- package/skills/applying-slds/examples.md +283 -0
- package/skills/applying-slds/guidance/README.md +83 -0
- package/skills/applying-slds/guidance/blueprints-index.md +213 -0
- package/skills/applying-slds/guidance/icons-guidance.md +186 -0
- package/skills/applying-slds/guidance/overviews/borders.md +236 -0
- package/skills/applying-slds/guidance/overviews/color.md +266 -0
- package/skills/applying-slds/guidance/overviews/display-density.md +366 -0
- package/skills/applying-slds/guidance/overviews/icons.md +240 -0
- package/skills/applying-slds/guidance/overviews/illustrations.md +235 -0
- package/skills/applying-slds/guidance/overviews/shadows.md +176 -0
- package/skills/applying-slds/guidance/overviews/spacing.md +216 -0
- package/skills/applying-slds/guidance/overviews/typography.md +323 -0
- package/skills/applying-slds/guidance/overviews/utilities.md +542 -0
- package/skills/applying-slds/guidance/slds-development-guide.md +288 -0
- package/skills/applying-slds/guidance/styling-hooks/borders.md +202 -0
- package/skills/applying-slds/guidance/styling-hooks/color/expressive-palette-hooks.md +153 -0
- package/skills/applying-slds/guidance/styling-hooks/color/index.md +171 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/accent-hooks.md +204 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/feedback-hooks.md +768 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/surface-hooks.md +337 -0
- package/skills/applying-slds/guidance/styling-hooks/color/system-hooks.md +132 -0
- package/skills/applying-slds/guidance/styling-hooks/index.md +327 -0
- package/skills/applying-slds/guidance/styling-hooks/shadows.md +238 -0
- package/skills/applying-slds/guidance/styling-hooks/spacing.md +254 -0
- package/skills/applying-slds/guidance/styling-hooks/typography.md +448 -0
- package/skills/applying-slds/guidance/utilities/alignment.md +119 -0
- package/skills/applying-slds/guidance/utilities/borders.md +131 -0
- package/skills/applying-slds/guidance/utilities/box.md +125 -0
- package/skills/applying-slds/guidance/utilities/color.md +165 -0
- package/skills/applying-slds/guidance/utilities/dark-mode.md +111 -0
- package/skills/applying-slds/guidance/utilities/description-list.md +168 -0
- package/skills/applying-slds/guidance/utilities/floats.md +117 -0
- package/skills/applying-slds/guidance/utilities/grid.md +264 -0
- package/skills/applying-slds/guidance/utilities/horizontal-list.md +110 -0
- package/skills/applying-slds/guidance/utilities/hyphenation.md +84 -0
- package/skills/applying-slds/guidance/utilities/index.md +205 -0
- package/skills/applying-slds/guidance/utilities/interactions.md +89 -0
- package/skills/applying-slds/guidance/utilities/layout.md +109 -0
- package/skills/applying-slds/guidance/utilities/line-clamp.md +131 -0
- package/skills/applying-slds/guidance/utilities/margin.md +155 -0
- package/skills/applying-slds/guidance/utilities/media-object.md +161 -0
- package/skills/applying-slds/guidance/utilities/name-value-list.md +152 -0
- package/skills/applying-slds/guidance/utilities/padding.md +155 -0
- package/skills/applying-slds/guidance/utilities/position.md +177 -0
- package/skills/applying-slds/guidance/utilities/print.md +114 -0
- package/skills/applying-slds/guidance/utilities/scrollable.md +126 -0
- package/skills/applying-slds/guidance/utilities/sizing.md +190 -0
- package/skills/applying-slds/guidance/utilities/themes.md +121 -0
- package/skills/applying-slds/guidance/utilities/truncate.md +127 -0
- package/skills/applying-slds/guidance/utilities/typography.md +166 -0
- package/skills/applying-slds/guidance/utilities/vertical-list.md +166 -0
- package/skills/applying-slds/guidance/utilities/visibility.md +228 -0
- package/skills/applying-slds/metadata/README.md +84 -0
- package/skills/applying-slds/metadata/blueprints/components/accordion.yaml +304 -0
- package/skills/applying-slds/metadata/blueprints/components/activity-timeline.yaml +92 -0
- package/skills/applying-slds/metadata/blueprints/components/alert.yaml +103 -0
- package/skills/applying-slds/metadata/blueprints/components/app-launcher.yaml +94 -0
- package/skills/applying-slds/metadata/blueprints/components/avatar-group.yaml +81 -0
- package/skills/applying-slds/metadata/blueprints/components/avatar.yaml +97 -0
- package/skills/applying-slds/metadata/blueprints/components/badges.yaml +102 -0
- package/skills/applying-slds/metadata/blueprints/components/brand-band.yaml +198 -0
- package/skills/applying-slds/metadata/blueprints/components/breadcrumbs.yaml +95 -0
- package/skills/applying-slds/metadata/blueprints/components/builder-header.yaml +192 -0
- package/skills/applying-slds/metadata/blueprints/components/button-groups.yaml +82 -0
- package/skills/applying-slds/metadata/blueprints/components/button-icons.yaml +295 -0
- package/skills/applying-slds/metadata/blueprints/components/buttons.yaml +230 -0
- package/skills/applying-slds/metadata/blueprints/components/cards.yaml +124 -0
- package/skills/applying-slds/metadata/blueprints/components/carousel.yaml +140 -0
- package/skills/applying-slds/metadata/blueprints/components/chat.yaml +179 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-button-group.yaml +192 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-button.yaml +204 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-toggle.yaml +177 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox.yaml +108 -0
- package/skills/applying-slds/metadata/blueprints/components/color-picker.yaml +172 -0
- package/skills/applying-slds/metadata/blueprints/components/combobox.yaml +136 -0
- package/skills/applying-slds/metadata/blueprints/components/counter.yaml +147 -0
- package/skills/applying-slds/metadata/blueprints/components/data-tables.yaml +157 -0
- package/skills/applying-slds/metadata/blueprints/components/datepickers.yaml +130 -0
- package/skills/applying-slds/metadata/blueprints/components/datetime-picker.yaml +155 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-composer.yaml +201 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-form-footer.yaml +161 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-utility-bar.yaml +175 -0
- package/skills/applying-slds/metadata/blueprints/components/drop-zone.yaml +115 -0
- package/skills/applying-slds/metadata/blueprints/components/dueling-picklist.yaml +196 -0
- package/skills/applying-slds/metadata/blueprints/components/dynamic-icons.yaml +128 -0
- package/skills/applying-slds/metadata/blueprints/components/dynamic-menu.yaml +141 -0
- package/skills/applying-slds/metadata/blueprints/components/expandable-section.yaml +115 -0
- package/skills/applying-slds/metadata/blueprints/components/expression.yaml +143 -0
- package/skills/applying-slds/metadata/blueprints/components/feeds.yaml +125 -0
- package/skills/applying-slds/metadata/blueprints/components/file-selector.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/files.yaml +119 -0
- package/skills/applying-slds/metadata/blueprints/components/form-element.yaml +145 -0
- package/skills/applying-slds/metadata/blueprints/components/global-header.yaml +120 -0
- package/skills/applying-slds/metadata/blueprints/components/global-navigation.yaml +100 -0
- package/skills/applying-slds/metadata/blueprints/components/icons.yaml +138 -0
- package/skills/applying-slds/metadata/blueprints/components/illustration.yaml +205 -0
- package/skills/applying-slds/metadata/blueprints/components/input.yaml +151 -0
- package/skills/applying-slds/metadata/blueprints/components/list-builder.yaml +127 -0
- package/skills/applying-slds/metadata/blueprints/components/lookups.yaml +132 -0
- package/skills/applying-slds/metadata/blueprints/components/map.yaml +118 -0
- package/skills/applying-slds/metadata/blueprints/components/menus.yaml +134 -0
- package/skills/applying-slds/metadata/blueprints/components/modals.yaml +152 -0
- package/skills/applying-slds/metadata/blueprints/components/notifications.yaml +88 -0
- package/skills/applying-slds/metadata/blueprints/components/page-headers.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/panels.yaml +149 -0
- package/skills/applying-slds/metadata/blueprints/components/path.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/picklist.yaml +125 -0
- package/skills/applying-slds/metadata/blueprints/components/pills.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/popovers.yaml +120 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-bar.yaml +110 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-indicator.yaml +133 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-ring.yaml +102 -0
- package/skills/applying-slds/metadata/blueprints/components/prompt.yaml +126 -0
- package/skills/applying-slds/metadata/blueprints/components/publishers.yaml +178 -0
- package/skills/applying-slds/metadata/blueprints/components/radio-button-group.yaml +172 -0
- package/skills/applying-slds/metadata/blueprints/components/radio-group.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/rich-text-editor.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/scoped-notifications.yaml +188 -0
- package/skills/applying-slds/metadata/blueprints/components/scoped-tabs.yaml +97 -0
- package/skills/applying-slds/metadata/blueprints/components/select.yaml +127 -0
- package/skills/applying-slds/metadata/blueprints/components/setup-assistant.yaml +152 -0
- package/skills/applying-slds/metadata/blueprints/components/slider.yaml +111 -0
- package/skills/applying-slds/metadata/blueprints/components/spinners.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/split-view.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/summary-detail.yaml +103 -0
- package/skills/applying-slds/metadata/blueprints/components/tabs.yaml +138 -0
- package/skills/applying-slds/metadata/blueprints/components/textarea.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/tiles.yaml +108 -0
- package/skills/applying-slds/metadata/blueprints/components/timepicker.yaml +111 -0
- package/skills/applying-slds/metadata/blueprints/components/toast.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/tooltips.yaml +107 -0
- package/skills/applying-slds/metadata/blueprints/components/tree-grid.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/trees.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/trial-bar.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/vertical-navigation.yaml +130 -0
- package/skills/applying-slds/metadata/blueprints/components/vertical-tabs.yaml +140 -0
- package/skills/applying-slds/metadata/blueprints/components/visual-picker.yaml +150 -0
- package/skills/applying-slds/metadata/blueprints/components/welcome-mat.yaml +136 -0
- package/skills/applying-slds/metadata/hooks-index.json +6272 -0
- package/skills/applying-slds/metadata/icon-metadata.json +38466 -0
- package/skills/applying-slds/metadata/utilities-index.json +21912 -0
- package/skills/applying-slds/references/component-selection.md +112 -0
- package/skills/applying-slds/references/icons-decision-guide.md +124 -0
- package/skills/applying-slds/references/styling-decision-guide.md +228 -0
- package/skills/applying-slds/references/utilities-quick-ref.md +125 -0
- package/skills/applying-slds/scripts/search-blueprints.cjs +117 -0
- package/skills/applying-slds/scripts/search-hooks.cjs +139 -0
- package/skills/applying-slds/scripts/search-icons.cjs +174 -0
- package/skills/applying-slds/scripts/search-utilities.cjs +161 -0
- package/skills/building-mobile-apps/SKILL.md +0 -1
- package/skills/building-omnistudio-callable-apex/SKILL.md +0 -1
- package/skills/building-omnistudio-datamapper/SKILL.md +0 -1
- package/skills/building-omnistudio-flexcard/SKILL.md +0 -1
- package/skills/building-omnistudio-integration-procedure/SKILL.md +0 -1
- package/skills/building-omnistudio-omniscript/SKILL.md +0 -1
- package/skills/building-sf-integrations/SKILL.md +0 -1
- package/skills/configuring-connected-apps/SKILL.md +0 -1
- package/skills/connecting-datacloud/SKILL.md +0 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +0 -1
- package/skills/debugging-apex-logs/SKILL.md +0 -1
- package/skills/deploying-metadata/SKILL.md +0 -1
- package/skills/deploying-omnistudio-datapacks/SKILL.md +0 -1
- package/skills/developing-agentforce/SKILL.md +0 -1
- package/skills/fetching-salesforce-docs/SKILL.md +0 -1
- package/skills/generating-custom-lightning-type/SKILL.md +17 -39
- package/skills/generating-custom-lightning-type/assets/primitive-types-and-constraints.md +41 -0
- package/skills/generating-custom-lightning-type/references/widget-rendition.md +124 -0
- package/skills/generating-lwc-components/SKILL.md +0 -1
- package/skills/generating-mermaid-diagrams/SKILL.md +0 -1
- package/skills/generating-visual-diagrams/SKILL.md +0 -1
- package/skills/handling-sf-data/SKILL.md +0 -1
- package/skills/harmonizing-datacloud/SKILL.md +0 -1
- package/skills/integrating-b2b-commerce-open-code-components/SKILL.md +0 -1
- package/skills/investigating-agentforce-architecture/README.md +156 -0
- package/skills/investigating-agentforce-architecture/SKILL.md +230 -0
- package/skills/investigating-agentforce-architecture/assets/cli/describe_sobject.yaml +16 -0
- package/skills/investigating-agentforce-architecture/assets/cli/describe_tooling_sobject.yaml +17 -0
- package/skills/investigating-agentforce-architecture/assets/cli/list_metadata_genaiprompttemplate.yaml +17 -0
- package/skills/investigating-agentforce-architecture/assets/cli/org_display.yaml +15 -0
- package/skills/investigating-agentforce-architecture/assets/cli/retrieve_genai_plugin.yaml +18 -0
- package/skills/investigating-agentforce-architecture/assets/cli/show_access_token.yaml +27 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/action_tree.mmd +20 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/data_flow.mmd +19 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/dependency_graph.mmd +19 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/invocation_sequence.mmd +20 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/planner_state.mmd +18 -0
- package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_names.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/bot_definition_details.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/bot_version_lookup.soql +4 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_by_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_ids_by_names.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_view_by_durable_ids.soql +4 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_metadata_by_id.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/functions_by_plugins.soql +5 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_attrs_by_parent_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_bundle_functions.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_definition_by_agent_chain.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugin_functions_by_plugin_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugin_instructions_by_plugin_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugins_by_planner.soql +4 -0
- package/skills/investigating-agentforce-architecture/references/architecture_sections.md +243 -0
- package/skills/investigating-agentforce-architecture/references/contract.json +244 -0
- package/skills/investigating-agentforce-architecture/references/soql_fields.md +512 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/__init__.py +1 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/fs_guard.py +329 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/paths.py +110 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/runtime.py +59 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/sql.py +10 -0
- package/skills/investigating-agentforce-architecture/scripts/cache_check.py +234 -0
- package/skills/investigating-agentforce-architecture/scripts/config.py +131 -0
- package/skills/investigating-agentforce-architecture/scripts/fetch_soql.py +689 -0
- package/skills/investigating-agentforce-architecture/scripts/finalize.py +295 -0
- package/skills/investigating-agentforce-architecture/scripts/main.py +2835 -0
- package/skills/investigating-agentforce-architecture/scripts/metadata_listing.py +265 -0
- package/skills/investigating-agentforce-architecture/scripts/parallel_retrieve.py +69 -0
- package/skills/investigating-agentforce-architecture/scripts/parse_bundle.py +215 -0
- package/skills/investigating-agentforce-architecture/scripts/parse_wave.py +845 -0
- package/skills/investigating-agentforce-architecture/scripts/probe_channels.py +302 -0
- package/skills/investigating-agentforce-architecture/scripts/render_architecture.py +1043 -0
- package/skills/investigating-agentforce-architecture/scripts/resolve_bot.py +255 -0
- package/skills/investigating-agentforce-architecture/scripts/resolve_invocation_target.py +130 -0
- package/skills/investigating-agentforce-architecture/scripts/rest_client.py +763 -0
- package/skills/investigating-agentforce-architecture/scripts/retrieve_planner.py +13 -0
- package/skills/investigating-agentforce-architecture/scripts/sf_cli.py +242 -0
- package/skills/investigating-agentforce-architecture/scripts/soql_loader.py +253 -0
- package/skills/investigating-agentforce-architecture/scripts/summarize_tree.py +143 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/__init__.py +0 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/_bootstrap.py +23 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/__init__.py +0 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/genai_payloads.py +400 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check.py +307 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check_main.py +283 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_config.py +115 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_end_to_end_fixture.py +651 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_finalize.py +278 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_flow_children_inflation.py +582 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_fs_guard.py +113 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_iterative_wave_b.py +478 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_main_pipeline.py +3359 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parallel_retrieve.py +131 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_bundle.py +400 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave.py +644 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_classifiers.py +224 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_helpers.py +380 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_main.py +397 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_per_branch_visited.py +244 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_channels.py +359 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_cli_recipes.py +185 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_render_architecture.py +810 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_bot.py +203 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_creds.py +157 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_invocation_target.py +145 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_rest_client.py +1253 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_runtime_override.py +100 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_sf_cli.py +261 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_signature_stamping.py +466 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_soql_loader.py +501 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_summarize_tree.py +241 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_write_emit_ctx.py +480 -0
- package/skills/investigating-agentforce-architecture/tools/emit_env.py +157 -0
- package/skills/investigating-agentforce-architecture/tools/emit_result.py +262 -0
- package/skills/investigating-agentforce-architecture/tools/sanitize.py +33 -0
- package/skills/investigating-agentforce-architecture/tools/write_emit_ctx.py +332 -0
- package/skills/investigating-agentforce-d360/README.md +123 -0
- package/skills/investigating-agentforce-d360/SKILL.md +163 -0
- package/skills/investigating-agentforce-d360/assets/dc/app_generation.sql +51 -0
- package/skills/investigating-agentforce-d360/assets/dc/content_category.sql +44 -0
- package/skills/investigating-agentforce-d360/assets/dc/content_quality.sql +41 -0
- package/skills/investigating-agentforce-d360/assets/dc/discover_sessions.sql +36 -0
- package/skills/investigating-agentforce-d360/assets/dc/feedback.sql +47 -0
- package/skills/investigating-agentforce-d360/assets/dc/feedback_details.sql +38 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_records.sql +45 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_llm.sql +50 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_metadata.sql +44 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_tags.sql +42 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_requests.sql +89 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_responses.sql +43 -0
- package/skills/investigating-agentforce-d360/assets/dc/generations.sql +52 -0
- package/skills/investigating-agentforce-d360/assets/dc/interactions.sql +53 -0
- package/skills/investigating-agentforce-d360/assets/dc/messages.sql +53 -0
- package/skills/investigating-agentforce-d360/assets/dc/messaging_session.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/moment_interactions.sql +34 -0
- package/skills/investigating-agentforce-d360/assets/dc/moments.sql +39 -0
- package/skills/investigating-agentforce-d360/assets/dc/participants.sql +48 -0
- package/skills/investigating-agentforce-d360/assets/dc/sessions.sql +78 -0
- package/skills/investigating-agentforce-d360/assets/dc/steps.sql +64 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_associations.sql +46 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_definition_associations.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_definitions.sql +50 -0
- package/skills/investigating-agentforce-d360/assets/dc/tags.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/telemetry_spans.sql +55 -0
- package/skills/investigating-agentforce-d360/references/artifacts.md +50 -0
- package/skills/investigating-agentforce-d360/references/dc_dmo_fields.md +823 -0
- package/skills/investigating-agentforce-d360/references/dc_pipeline_contract.md +608 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/__init__.py +2 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/cli_override.py +98 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/fs_guard.py +334 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/paths.py +155 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/runtime.py +59 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/sql.py +14 -0
- package/skills/investigating-agentforce-d360/scripts/assemble_dc.py +1624 -0
- package/skills/investigating-agentforce-d360/scripts/config.py +45 -0
- package/skills/investigating-agentforce-d360/scripts/dc.py +188 -0
- package/skills/investigating-agentforce-d360/scripts/discover_sessions.py +556 -0
- package/skills/investigating-agentforce-d360/scripts/fetch_dc.py +1045 -0
- package/skills/investigating-agentforce-d360/scripts/render_dc.py +1750 -0
- package/skills/investigating-agentforce-d360/scripts/resolve_session.py +264 -0
- package/skills/investigating-agentforce-d360/scripts/storage.py +92 -0
- package/skills/investigating-agentforce-d360/scripts/tests/__init__.py +0 -0
- package/skills/investigating-agentforce-d360/scripts/tests/_bootstrap.py +15 -0
- package/skills/investigating-agentforce-d360/scripts/tests/fixtures/__init__.py +0 -0
- package/skills/investigating-agentforce-d360/scripts/tests/fixtures/synthetic_session.py +424 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_bootstrap_and_mode.py +115 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct.py +220 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct_integration.py +158 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_helpers.py +287 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_integration.py +247 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_dc_and_resolve_session.py +433 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions.py +458 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions_grep_ci.py +193 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_helpers.py +266 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_identity.py +528 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_main.py +251 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall.py +229 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall_full.py +283 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_identity_coherence.py +327 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_branches.py +256 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_gateway_direct.py +130 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_helpers.py +291 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_integration.py +220 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_planner_llm_calls.py +284 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_show_prompts_gating.py +215 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_from_disk.py +100 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_session_main.py +149 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_runtime_override.py +104 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape.py +95 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape_dropped_by_stdm.py +85 -0
- package/skills/managing-managed-event-subscription/SKILL.md +152 -0
- package/skills/managing-managed-event-subscription/assets/managed-event-subscription-template.xml +20 -0
- package/skills/managing-managed-event-subscription/references/delete-guide.md +57 -0
- package/skills/managing-managed-event-subscription/references/topic-name-formats.md +26 -0
- package/skills/managing-managed-event-subscription/references/update-constraints.md +30 -0
- package/skills/modeling-omnistudio-epc-catalog/SKILL.md +0 -1
- package/skills/observing-agentforce/SKILL.md +0 -1
- package/skills/orchestrating-datacloud/SKILL.md +0 -1
- package/skills/preparing-datacloud/SKILL.md +0 -1
- package/skills/querying-soql/SKILL.md +0 -1
- package/skills/retrieving-datacloud/SKILL.md +0 -1
- package/skills/running-apex-tests/SKILL.md +0 -1
- package/skills/running-code-analyzer/SKILL.md +0 -1
- package/skills/segmenting-datacloud/SKILL.md +0 -1
- package/skills/testing-agentforce/SKILL.md +0 -1
- package/skills/uplifting-components-to-slds2/SKILL.md +3 -2
- package/skills/uplifting-components-to-slds2/references/color-hooks-decision-guide.md +30 -9
- package/skills/uplifting-components-to-slds2/references/examples.md +24 -6
- package/skills/validating-slds/SKILL.md +262 -0
- package/skills/validating-slds/references/quality-checks.md +308 -0
- package/skills/validating-slds/references/report-format.md +302 -0
- 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()
|