@salesforce/afv-skills 1.13.0 → 1.15.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 +3 -3
- 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-ui-bundle-app/SKILL.md +33 -8
- package/skills/generating-custom-application/SKILL.md +1 -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-ui-bundle-custom-app/SKILL.md +93 -0
- package/skills/generating-ui-bundle-custom-app/docs/configure-metadata-custom-application.md +70 -0
- package/skills/generating-ui-bundle-metadata/SKILL.md +39 -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/reviewing-lwc-mobile-offline/SKILL.md +168 -0
- package/skills/reviewing-lwc-mobile-offline/references/grounding.md +7 -0
- package/skills/reviewing-lwc-mobile-offline/references/inline-graphql.md +43 -0
- package/skills/reviewing-lwc-mobile-offline/references/komaci-eslint.md +125 -0
- package/skills/reviewing-lwc-mobile-offline/references/lwc-if.md +78 -0
- package/skills/reviewing-lwc-mobile-offline/scripts/komaci.config.mjs +18 -0
- package/skills/reviewing-lwc-mobile-offline/scripts/package.json +10 -0
- package/skills/reviewing-lwc-mobile-offline/scripts/run-komaci.sh +69 -0
- 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/using-mobile-native-capabilities/SKILL.md +182 -0
- package/skills/using-mobile-native-capabilities/references/app-review.md +68 -0
- package/skills/using-mobile-native-capabilities/references/ar-space-capture.md +125 -0
- package/skills/using-mobile-native-capabilities/references/barcode-scanner.md +219 -0
- package/skills/using-mobile-native-capabilities/references/base-capability.md +22 -0
- package/skills/using-mobile-native-capabilities/references/biometrics.md +90 -0
- package/skills/using-mobile-native-capabilities/references/calendar.md +213 -0
- package/skills/using-mobile-native-capabilities/references/contacts.md +232 -0
- package/skills/using-mobile-native-capabilities/references/document-scanner.md +342 -0
- package/skills/using-mobile-native-capabilities/references/geofencing.md +123 -0
- package/skills/using-mobile-native-capabilities/references/location.md +158 -0
- package/skills/using-mobile-native-capabilities/references/mobile-capabilities.md +30 -0
- package/skills/using-mobile-native-capabilities/references/nfc.md +181 -0
- package/skills/using-mobile-native-capabilities/references/payments.md +95 -0
- 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,359 @@
|
|
|
1
|
+
"""Tests for channel probe TTL (7d), mandatory-field gate,
|
|
2
|
+
runtime invalidate + re-probe.
|
|
3
|
+
|
|
4
|
+
Every test mocks `sf_cli.run_sf` — no real org calls. Cache dir is
|
|
5
|
+
redirected into a tempdir via monkey-patching `config.PROBE_CACHE_ROOT`
|
|
6
|
+
to keep tests hermetic.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
import unittest
|
|
17
|
+
import unittest.mock as mock
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from . import _bootstrap # noqa: F401
|
|
21
|
+
|
|
22
|
+
import config # type: ignore
|
|
23
|
+
import probe_channels # type: ignore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fake_describe_ok(fields: list[str]) -> dict:
|
|
27
|
+
"""Shape the sf_cli.run_sf output: success envelope wrapping a fields list."""
|
|
28
|
+
return {
|
|
29
|
+
"status": 0,
|
|
30
|
+
"result": {
|
|
31
|
+
"fields": [{"name": f, "queryable": True} for f in fields],
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# A "universal" success payload: every mandatory field for every sObject
|
|
37
|
+
# present. Individual tests override for specific sObjects.
|
|
38
|
+
def _all_fields_ok_run_sf(recipe: str, **params: str) -> dict:
|
|
39
|
+
sobject = params["SOBJECT"]
|
|
40
|
+
mandatory = probe_channels.MANDATORY_FIELDS.get(sobject, set())
|
|
41
|
+
# Add a couple of bonus fields so queryable_fields isn't identical to
|
|
42
|
+
# mandatory_missing-inverse by construction.
|
|
43
|
+
extra = {"CreatedDate", "LastModifiedDate"}
|
|
44
|
+
return _fake_describe_ok(sorted(mandatory | extra))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ProbeChannelsCacheTests(unittest.TestCase):
|
|
48
|
+
"""Cache hit / miss / TTL semantics."""
|
|
49
|
+
|
|
50
|
+
def setUp(self) -> None:
|
|
51
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
52
|
+
self.addCleanup(self._tmp.cleanup)
|
|
53
|
+
self._orig_root = config.PROBE_CACHE_ROOT
|
|
54
|
+
config.PROBE_CACHE_ROOT = Path(self._tmp.name) / "_channel_probe"
|
|
55
|
+
|
|
56
|
+
def tearDown(self) -> None:
|
|
57
|
+
config.PROBE_CACHE_ROOT = self._orig_root
|
|
58
|
+
|
|
59
|
+
def test_cache_hit_within_ttl_no_network(self):
|
|
60
|
+
"""A fresh cache file + force_refresh=False → no run_sf calls."""
|
|
61
|
+
cache_dir = config.build_probe_cache_dir("00D000000000ABC", "v60.0")
|
|
62
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
cache_file = cache_dir / "channels.json"
|
|
64
|
+
payload = {
|
|
65
|
+
"_schema": "channels/1",
|
|
66
|
+
"_built_at_utc": time.time(), # right now → fresh
|
|
67
|
+
"status": "OK",
|
|
68
|
+
"channels": {"BotDefinition": {"queryable_fields": ["Id"],
|
|
69
|
+
"mandatory_missing": [],
|
|
70
|
+
"describe_error": None}},
|
|
71
|
+
}
|
|
72
|
+
cache_file.write_text(json.dumps(payload))
|
|
73
|
+
|
|
74
|
+
with mock.patch.object(probe_channels, "run_sf") as m:
|
|
75
|
+
out = probe_channels.probe_channels(
|
|
76
|
+
"myorg", "00D000000000ABC", "v60.0", force_refresh=False,
|
|
77
|
+
)
|
|
78
|
+
self.assertEqual(out["status"], "OK")
|
|
79
|
+
m.assert_not_called()
|
|
80
|
+
|
|
81
|
+
def test_cache_stale_triggers_reprobe(self):
|
|
82
|
+
"""Cached file older than TTL → re-run describes."""
|
|
83
|
+
cache_dir = config.build_probe_cache_dir("00D000000000ABC", "v60.0")
|
|
84
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
cache_file = cache_dir / "channels.json"
|
|
86
|
+
stale = {
|
|
87
|
+
"_schema": "channels/1",
|
|
88
|
+
"_built_at_utc": time.time() - (8 * 86400), # 8 days old
|
|
89
|
+
"status": "OK",
|
|
90
|
+
"channels": {},
|
|
91
|
+
}
|
|
92
|
+
cache_file.write_text(json.dumps(stale))
|
|
93
|
+
|
|
94
|
+
with mock.patch.object(probe_channels, "run_sf",
|
|
95
|
+
side_effect=_all_fields_ok_run_sf) as m:
|
|
96
|
+
out = probe_channels.probe_channels(
|
|
97
|
+
"myorg", "00D000000000ABC", "v60.0", force_refresh=False,
|
|
98
|
+
)
|
|
99
|
+
self.assertEqual(out["status"], "OK")
|
|
100
|
+
# One call per sObject.
|
|
101
|
+
self.assertEqual(m.call_count, len(probe_channels.ALL_SOBJECTS))
|
|
102
|
+
|
|
103
|
+
def test_force_refresh_always_reprobes(self):
|
|
104
|
+
"""force_refresh=True bypasses a fresh cache."""
|
|
105
|
+
cache_dir = config.build_probe_cache_dir("00D000000000ABC", "v60.0")
|
|
106
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
cache_file = cache_dir / "channels.json"
|
|
108
|
+
fresh = {
|
|
109
|
+
"_schema": "channels/1",
|
|
110
|
+
"_built_at_utc": time.time(),
|
|
111
|
+
"status": "OK",
|
|
112
|
+
"channels": {},
|
|
113
|
+
}
|
|
114
|
+
cache_file.write_text(json.dumps(fresh))
|
|
115
|
+
|
|
116
|
+
with mock.patch.object(probe_channels, "run_sf",
|
|
117
|
+
side_effect=_all_fields_ok_run_sf) as m:
|
|
118
|
+
probe_channels.probe_channels(
|
|
119
|
+
"myorg", "00D000000000ABC", "v60.0", force_refresh=True,
|
|
120
|
+
)
|
|
121
|
+
self.assertEqual(m.call_count, len(probe_channels.ALL_SOBJECTS))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class InvalidateAndReprobeTests(unittest.TestCase):
|
|
125
|
+
"""runtime escape valve when SOQL returns INVALID_FIELD."""
|
|
126
|
+
|
|
127
|
+
def setUp(self) -> None:
|
|
128
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
129
|
+
self.addCleanup(self._tmp.cleanup)
|
|
130
|
+
self._orig_root = config.PROBE_CACHE_ROOT
|
|
131
|
+
config.PROBE_CACHE_ROOT = Path(self._tmp.name) / "_channel_probe"
|
|
132
|
+
|
|
133
|
+
def tearDown(self) -> None:
|
|
134
|
+
config.PROBE_CACHE_ROOT = self._orig_root
|
|
135
|
+
|
|
136
|
+
def test_invalidate_deletes_cache_and_reruns(self):
|
|
137
|
+
cache_dir = config.build_probe_cache_dir("00D000000000ABC", "v60.0")
|
|
138
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
cache_file = cache_dir / "channels.json"
|
|
140
|
+
cache_file.write_text(json.dumps({
|
|
141
|
+
"_schema": "channels/1",
|
|
142
|
+
"_built_at_utc": time.time(),
|
|
143
|
+
"status": "OK",
|
|
144
|
+
"channels": {},
|
|
145
|
+
}))
|
|
146
|
+
|
|
147
|
+
with mock.patch.object(probe_channels, "run_sf",
|
|
148
|
+
side_effect=_all_fields_ok_run_sf) as m:
|
|
149
|
+
out = probe_channels.invalidate_and_reprobe(
|
|
150
|
+
"myorg", "00D000000000ABC", "v60.0",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
self.assertEqual(out["status"], "OK")
|
|
154
|
+
self.assertTrue(cache_file.is_file(), "fresh cache must be rewritten")
|
|
155
|
+
self.assertEqual(m.call_count, len(probe_channels.ALL_SOBJECTS))
|
|
156
|
+
|
|
157
|
+
def test_invalidate_tolerates_missing_cache(self):
|
|
158
|
+
"""Cold-path invalidate (no file yet) must not crash."""
|
|
159
|
+
with mock.patch.object(probe_channels, "run_sf",
|
|
160
|
+
side_effect=_all_fields_ok_run_sf):
|
|
161
|
+
out = probe_channels.invalidate_and_reprobe(
|
|
162
|
+
"myorg", "00D000000000ABC", "v60.0",
|
|
163
|
+
)
|
|
164
|
+
self.assertEqual(out["status"], "OK")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class MandatoryFieldGateTests(unittest.TestCase):
|
|
168
|
+
"""Missing mandatory field → status flips to PROBE_FAILED."""
|
|
169
|
+
|
|
170
|
+
def setUp(self) -> None:
|
|
171
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
172
|
+
self.addCleanup(self._tmp.cleanup)
|
|
173
|
+
self._orig_root = config.PROBE_CACHE_ROOT
|
|
174
|
+
config.PROBE_CACHE_ROOT = Path(self._tmp.name) / "_channel_probe"
|
|
175
|
+
|
|
176
|
+
def tearDown(self) -> None:
|
|
177
|
+
config.PROBE_CACHE_ROOT = self._orig_root
|
|
178
|
+
|
|
179
|
+
def test_missing_invocation_target_flags_probe_failed(self):
|
|
180
|
+
"""SF release drops `InvocationTarget` on
|
|
181
|
+
GenAiFunctionDefinition → probe must surface PROBE_FAILED.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def mock_run_sf(recipe: str, **params: str) -> dict:
|
|
185
|
+
sobject = params["SOBJECT"]
|
|
186
|
+
mandatory = set(probe_channels.MANDATORY_FIELDS.get(sobject, set()))
|
|
187
|
+
if sobject == "GenAiFunctionDefinition":
|
|
188
|
+
# Simulate the schema drift: drop InvocationTarget.
|
|
189
|
+
mandatory.discard("InvocationTarget")
|
|
190
|
+
return _fake_describe_ok(sorted(mandatory))
|
|
191
|
+
|
|
192
|
+
with mock.patch.object(probe_channels, "run_sf", side_effect=mock_run_sf):
|
|
193
|
+
out = probe_channels.probe_channels(
|
|
194
|
+
"myorg", "00D000000000ABC", "v60.0", force_refresh=True,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
self.assertEqual(out["status"], "PROBE_FAILED")
|
|
198
|
+
entry = out["channels"]["GenAiFunctionDefinition"]
|
|
199
|
+
self.assertIn("InvocationTarget", entry["mandatory_missing"])
|
|
200
|
+
# Other sObjects unaffected.
|
|
201
|
+
self.assertEqual(
|
|
202
|
+
out["channels"]["BotDefinition"]["mandatory_missing"], [],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class CacheFilePermissionsTests(unittest.TestCase):
|
|
207
|
+
"""cached channels.json must be 0o600 (owner RW only)."""
|
|
208
|
+
|
|
209
|
+
def setUp(self) -> None:
|
|
210
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
211
|
+
self.addCleanup(self._tmp.cleanup)
|
|
212
|
+
self._orig_root = config.PROBE_CACHE_ROOT
|
|
213
|
+
config.PROBE_CACHE_ROOT = Path(self._tmp.name) / "_channel_probe"
|
|
214
|
+
|
|
215
|
+
def tearDown(self) -> None:
|
|
216
|
+
config.PROBE_CACHE_ROOT = self._orig_root
|
|
217
|
+
|
|
218
|
+
def test_cache_file_is_mode_0o600(self):
|
|
219
|
+
with mock.patch.object(probe_channels, "run_sf",
|
|
220
|
+
side_effect=_all_fields_ok_run_sf):
|
|
221
|
+
probe_channels.probe_channels(
|
|
222
|
+
"myorg", "00D000000000ABC", "v60.0", force_refresh=True,
|
|
223
|
+
)
|
|
224
|
+
cache_file = (
|
|
225
|
+
config.build_probe_cache_dir("00D000000000ABC", "v60.0")
|
|
226
|
+
/ "channels.json"
|
|
227
|
+
)
|
|
228
|
+
self.assertTrue(cache_file.is_file())
|
|
229
|
+
mode = stat.S_IMODE(cache_file.stat().st_mode)
|
|
230
|
+
self.assertEqual(
|
|
231
|
+
mode, 0o600,
|
|
232
|
+
f"cache file mode must be 0o600, got 0o{mode:03o} ",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class CacheFreshnessBoundsTests(unittest.TestCase):
|
|
237
|
+
"""`_is_cache_fresh` must reject future timestamps as stale.
|
|
238
|
+
|
|
239
|
+
A `_built_at_utc` in the future is a symptom of clock skew (NTP
|
|
240
|
+
drift after suspend, container clock jump) or cache tampering —
|
|
241
|
+
either way, we treat it as untrustworthy and force a reprobe.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def setUp(self) -> None:
|
|
245
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
246
|
+
self.addCleanup(self._tmp.cleanup)
|
|
247
|
+
|
|
248
|
+
def test_future_timestamp_is_not_fresh(self):
|
|
249
|
+
# Year 3000 → unambiguously future.
|
|
250
|
+
future_epoch = 32503680000.0
|
|
251
|
+
cache_file = Path(self._tmp.name) / "channels.json"
|
|
252
|
+
cache_file.write_text(json.dumps({
|
|
253
|
+
"_schema": "channels/1",
|
|
254
|
+
"_built_at_utc": future_epoch,
|
|
255
|
+
"status": "OK",
|
|
256
|
+
"channels": {},
|
|
257
|
+
}))
|
|
258
|
+
self.assertFalse(
|
|
259
|
+
probe_channels._is_cache_fresh(cache_file, ttl_days=7),
|
|
260
|
+
"future _built_at_utc must be rejected as stale",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def test_present_timestamp_is_fresh(self):
|
|
264
|
+
cache_file = Path(self._tmp.name) / "channels.json"
|
|
265
|
+
cache_file.write_text(json.dumps({
|
|
266
|
+
"_schema": "channels/1",
|
|
267
|
+
"_built_at_utc": time.time(),
|
|
268
|
+
"status": "OK",
|
|
269
|
+
"channels": {},
|
|
270
|
+
}))
|
|
271
|
+
self.assertTrue(probe_channels._is_cache_fresh(cache_file, ttl_days=7))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class WriteCacheConcurrencyTests(unittest.TestCase):
|
|
275
|
+
"""concurrent _write_cache calls must not collide on tmp names.
|
|
276
|
+
|
|
277
|
+
The prior design used `path.with_suffix(path.suffix + ".tmp")` — a
|
|
278
|
+
deterministic name shared by every concurrent writer. This test
|
|
279
|
+
fires N threads that all write the same target path; after the dust
|
|
280
|
+
settles there must be exactly one final file, no leftover `.tmp`
|
|
281
|
+
files, and the contents must be valid JSON from ONE of the writers
|
|
282
|
+
(any of them is acceptable — what matters is integrity).
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def setUp(self) -> None:
|
|
286
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
287
|
+
self.addCleanup(self._tmp.cleanup)
|
|
288
|
+
|
|
289
|
+
def test_concurrent_writes_no_collision_no_corruption(self):
|
|
290
|
+
import threading
|
|
291
|
+
|
|
292
|
+
target_dir = Path(self._tmp.name) / "probe"
|
|
293
|
+
target_path = target_dir / "channels.json"
|
|
294
|
+
|
|
295
|
+
N = 8
|
|
296
|
+
errors: list[BaseException] = []
|
|
297
|
+
barrier = threading.Barrier(N)
|
|
298
|
+
|
|
299
|
+
def worker(i: int) -> None:
|
|
300
|
+
payload = {
|
|
301
|
+
"_schema": "channels/1",
|
|
302
|
+
"_built_at_utc": float(i + 1),
|
|
303
|
+
"status": "OK",
|
|
304
|
+
"channels": {"writer": {"queryable_fields": [f"w{i}"]}},
|
|
305
|
+
}
|
|
306
|
+
barrier.wait() # maximise contention
|
|
307
|
+
try:
|
|
308
|
+
probe_channels._write_cache(target_path, payload)
|
|
309
|
+
except BaseException as e: # noqa: BLE001 — thread boundary
|
|
310
|
+
errors.append(e)
|
|
311
|
+
|
|
312
|
+
threads = [threading.Thread(target=worker, args=(i,)) for i in range(N)]
|
|
313
|
+
for t in threads:
|
|
314
|
+
t.start()
|
|
315
|
+
for t in threads:
|
|
316
|
+
t.join(timeout=5.0)
|
|
317
|
+
|
|
318
|
+
self.assertEqual(errors, [], f"worker errors: {errors}")
|
|
319
|
+
self.assertTrue(target_path.is_file())
|
|
320
|
+
|
|
321
|
+
# No stray tmp files left behind.
|
|
322
|
+
leftover = list(target_dir.glob(".channels.*.tmp"))
|
|
323
|
+
self.assertEqual(
|
|
324
|
+
leftover, [],
|
|
325
|
+
f"concurrent writes left tmp leftovers: {leftover}",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Final file is parseable JSON (not a half-written corruption).
|
|
329
|
+
parsed = json.loads(target_path.read_text())
|
|
330
|
+
# One of the N payloads won the race; its _built_at_utc is in [1, N].
|
|
331
|
+
self.assertIn(parsed["_built_at_utc"], [float(i + 1) for i in range(N)])
|
|
332
|
+
|
|
333
|
+
def test_parent_dir_permissions_are_0o700(self):
|
|
334
|
+
"""parent dir is normalized to 0o700 after write.
|
|
335
|
+
|
|
336
|
+
Even if the directory pre-existed with looser permissions, a
|
|
337
|
+
successful _write_cache leaves it at 0o700.
|
|
338
|
+
"""
|
|
339
|
+
target_path = Path(self._tmp.name) / "probe" / "channels.json"
|
|
340
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
# Deliberately widen perms to prove _write_cache narrows them.
|
|
342
|
+
os.chmod(target_path.parent, 0o755)
|
|
343
|
+
|
|
344
|
+
probe_channels._write_cache(target_path, {
|
|
345
|
+
"_schema": "channels/1",
|
|
346
|
+
"_built_at_utc": time.time(),
|
|
347
|
+
"status": "OK",
|
|
348
|
+
"channels": {},
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
mode = stat.S_IMODE(target_path.parent.stat().st_mode)
|
|
352
|
+
self.assertEqual(
|
|
353
|
+
mode, 0o700,
|
|
354
|
+
f"parent dir mode must be 0o700, got 0o{mode:03o}",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
if __name__ == "__main__":
|
|
359
|
+
unittest.main()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""structural validation of the describe CLI recipes.
|
|
2
|
+
|
|
3
|
+
Before these recipes shipped, `scripts/probe_channels.py::_describe_sobject`
|
|
4
|
+
called `run_sf("describe_sobject", ...)` / `run_sf("describe_tooling_sobject",
|
|
5
|
+
...)` against YAMLs that didn't exist on disk. Every unit test mocked
|
|
6
|
+
`run_sf`, so the suite stayed green — but the first real invocation would
|
|
7
|
+
have raised `SfCliError: recipe not found`. This module closes that gap
|
|
8
|
+
by loading each YAML via `sf_cli._load_recipe` and asserting the schema
|
|
9
|
+
`sf_cli.run_sf` expects.
|
|
10
|
+
|
|
11
|
+
Deliberately does NOT invoke `sf sobject describe` — that needs a live
|
|
12
|
+
org. Structural validation only; the wire-level contract is covered by
|
|
13
|
+
the existing probe_channels integration path once the skill ships.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import unittest
|
|
18
|
+
import unittest.mock as mock
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from . import _bootstrap # noqa: F401
|
|
22
|
+
|
|
23
|
+
import sf_cli # type: ignore
|
|
24
|
+
|
|
25
|
+
# SKILL_ROOT is now file-relative (config.py uses
|
|
26
|
+
# Path(__file__).resolve().parent.parent), so config.CLI_DIR auto-resolves
|
|
27
|
+
# to the repo's assets/cli/ under test. _REPO_CLI_DIR is kept for tests
|
|
28
|
+
# that compare paths or pass them as explicit args.
|
|
29
|
+
_REPO_CLI_DIR = (
|
|
30
|
+
Path(__file__).resolve().parent.parent.parent / "assets" / "cli"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DescribeRecipeStructureTests(unittest.TestCase):
|
|
35
|
+
"""Both describe recipes must parse + conform to the run_sf schema."""
|
|
36
|
+
|
|
37
|
+
def setUp(self) -> None:
|
|
38
|
+
# patch the module-level CLI_DIR binding inside sf_cli so
|
|
39
|
+
# _load_recipe reads from the repo's assets/cli/. `sf_cli`
|
|
40
|
+
# imported `CLI_DIR` by name at module load, so we must patch
|
|
41
|
+
# the *local* reference in sf_cli — patching `config.CLI_DIR`
|
|
42
|
+
# alone would leave the stale import behind.
|
|
43
|
+
self._patch = mock.patch.object(sf_cli, "CLI_DIR", _REPO_CLI_DIR)
|
|
44
|
+
self._patch.start()
|
|
45
|
+
self.addCleanup(self._patch.stop)
|
|
46
|
+
|
|
47
|
+
def _load(self, name: str) -> dict:
|
|
48
|
+
# _load_recipe is the same function run_sf uses at runtime, so if
|
|
49
|
+
# it parses here it will parse in production. yaml.safe_load + the
|
|
50
|
+
# mapping-shape guard both run inside _load_recipe .
|
|
51
|
+
return sf_cli._load_recipe(name)
|
|
52
|
+
|
|
53
|
+
# ---- parsing + mapping shape -----------------------------------------
|
|
54
|
+
|
|
55
|
+
def test_describe_sobject_parses(self):
|
|
56
|
+
recipe = self._load("describe_sobject")
|
|
57
|
+
self.assertIsInstance(recipe, dict)
|
|
58
|
+
self.assertEqual(recipe.get("name"), "describe_sobject")
|
|
59
|
+
|
|
60
|
+
def test_describe_tooling_sobject_parses(self):
|
|
61
|
+
recipe = self._load("describe_tooling_sobject")
|
|
62
|
+
self.assertIsInstance(recipe, dict)
|
|
63
|
+
self.assertEqual(recipe.get("name"), "describe_tooling_sobject")
|
|
64
|
+
|
|
65
|
+
# ---- required_params: a list, covers what probe_channels passes -------
|
|
66
|
+
|
|
67
|
+
def test_required_params_are_list_of_strings(self):
|
|
68
|
+
for name in ("describe_sobject", "describe_tooling_sobject"):
|
|
69
|
+
with self.subTest(recipe=name):
|
|
70
|
+
recipe = self._load(name)
|
|
71
|
+
required = recipe.get("required_params")
|
|
72
|
+
self.assertIsInstance(required, list)
|
|
73
|
+
self.assertTrue(
|
|
74
|
+
all(isinstance(p, str) for p in required),
|
|
75
|
+
f"{name}: required_params must be list[str]",
|
|
76
|
+
)
|
|
77
|
+
# probe_channels._describe_sobject passes exactly these two.
|
|
78
|
+
# If the recipe drops either, run_sf will SfCliError before
|
|
79
|
+
# the subprocess call — so assert both are present.
|
|
80
|
+
self.assertIn("ORG_ALIAS", required)
|
|
81
|
+
self.assertIn("SOBJECT", required)
|
|
82
|
+
|
|
83
|
+
# ---- argv: a list of strings with both placeholders referenced --------
|
|
84
|
+
|
|
85
|
+
def test_argv_is_list_of_strings(self):
|
|
86
|
+
for name in ("describe_sobject", "describe_tooling_sobject"):
|
|
87
|
+
with self.subTest(recipe=name):
|
|
88
|
+
recipe = self._load(name)
|
|
89
|
+
argv = recipe.get("argv")
|
|
90
|
+
self.assertIsInstance(argv, list)
|
|
91
|
+
self.assertGreater(len(argv), 0)
|
|
92
|
+
self.assertTrue(
|
|
93
|
+
all(isinstance(e, str) for e in argv),
|
|
94
|
+
f"{name}: argv must be list[str]",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def test_argv_references_both_required_placeholders(self):
|
|
98
|
+
"""Every key in required_params must appear as `{{KEY}}` somewhere
|
|
99
|
+
in argv — otherwise the substitution is a no-op and the caller's
|
|
100
|
+
input never reaches the CLI. Defensive sanity check, not a
|
|
101
|
+
functional run.
|
|
102
|
+
"""
|
|
103
|
+
for name in ("describe_sobject", "describe_tooling_sobject"):
|
|
104
|
+
with self.subTest(recipe=name):
|
|
105
|
+
recipe = self._load(name)
|
|
106
|
+
argv_joined = " ".join(recipe["argv"])
|
|
107
|
+
for key in recipe.get("required_params") or []:
|
|
108
|
+
self.assertIn(
|
|
109
|
+
f"{{{{{key}}}}}", argv_joined,
|
|
110
|
+
f"{name}: required param {key} not referenced in argv",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# ---- success_check contract -----------------------------------------
|
|
114
|
+
|
|
115
|
+
def test_success_check_is_stdout_json_status_zero(self):
|
|
116
|
+
"""run_sf's success-detection code path (in sf_cli.run_sf) assumes
|
|
117
|
+
`stdout_json_status_zero`. Any other value silently changes the
|
|
118
|
+
pass/fail contract, so pin it.
|
|
119
|
+
"""
|
|
120
|
+
for name in ("describe_sobject", "describe_tooling_sobject"):
|
|
121
|
+
with self.subTest(recipe=name):
|
|
122
|
+
recipe = self._load(name)
|
|
123
|
+
self.assertEqual(
|
|
124
|
+
recipe.get("success_check"), "stdout_json_status_zero",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ---- timeout is a positive int ---------------------------------------
|
|
128
|
+
|
|
129
|
+
def test_timeout_is_positive_int(self):
|
|
130
|
+
for name in ("describe_sobject", "describe_tooling_sobject"):
|
|
131
|
+
with self.subTest(recipe=name):
|
|
132
|
+
recipe = self._load(name)
|
|
133
|
+
t = recipe.get("timeout_seconds")
|
|
134
|
+
self.assertIsInstance(t, int)
|
|
135
|
+
self.assertGreater(t, 0)
|
|
136
|
+
|
|
137
|
+
# ---- auth_required_stderr_patterns -----------------------------------
|
|
138
|
+
|
|
139
|
+
def test_auth_patterns_cover_the_known_failures(self):
|
|
140
|
+
"""sf CLI surfaces `NoOrgAuthenticationError` for a never-logged-in
|
|
141
|
+
alias and `AuthInfoError` for a stale/revoked session. Both must
|
|
142
|
+
classify as AuthRequired, not a generic SfCliError, so the skill
|
|
143
|
+
can prompt the user to re-login instead of aborting.
|
|
144
|
+
"""
|
|
145
|
+
for name in ("describe_sobject", "describe_tooling_sobject"):
|
|
146
|
+
with self.subTest(recipe=name):
|
|
147
|
+
recipe = self._load(name)
|
|
148
|
+
patterns = recipe.get("auth_required_stderr_patterns") or []
|
|
149
|
+
self.assertIn("NoOrgAuthenticationError", patterns)
|
|
150
|
+
self.assertIn("AuthInfoError", patterns)
|
|
151
|
+
|
|
152
|
+
# ---- tooling recipe has --use-tooling-api; data-API recipe does NOT ---
|
|
153
|
+
|
|
154
|
+
def test_tooling_recipe_uses_tooling_api_flag(self):
|
|
155
|
+
recipe = self._load("describe_tooling_sobject")
|
|
156
|
+
self.assertIn("--use-tooling-api", recipe["argv"])
|
|
157
|
+
|
|
158
|
+
def test_data_api_recipe_does_not_use_tooling_api_flag(self):
|
|
159
|
+
"""Separating the two recipes rather than toggling via a param
|
|
160
|
+
was a deliberate design call (call sites stay explicit). Verify
|
|
161
|
+
the separation hasn't regressed into a single merged recipe.
|
|
162
|
+
"""
|
|
163
|
+
recipe = self._load("describe_sobject")
|
|
164
|
+
self.assertNotIn("--use-tooling-api", recipe["argv"])
|
|
165
|
+
|
|
166
|
+
# ---- run_sf missing-param surfacing runs end-to-end through recipe ----
|
|
167
|
+
|
|
168
|
+
def test_run_sf_raises_cleanly_when_required_param_absent(self):
|
|
169
|
+
"""This exercises the full load_recipe → required_params check
|
|
170
|
+
path inside run_sf, with a REAL (on-disk) recipe rather than a
|
|
171
|
+
mock. If the YAML parses but required_params is malformed, this
|
|
172
|
+
would surface as something other than SfCliError with a clear
|
|
173
|
+
'missing required params' message.
|
|
174
|
+
"""
|
|
175
|
+
with self.assertRaises(sf_cli.SfCliError) as ctx:
|
|
176
|
+
# Deliberately omit SOBJECT — should raise at the required-
|
|
177
|
+
# params check, well before subprocess.run. Pass ORG_ALIAS so
|
|
178
|
+
# we isolate the failure to the missing param.
|
|
179
|
+
sf_cli.run_sf("describe_sobject", ORG_ALIAS="whatever")
|
|
180
|
+
self.assertIn("missing required params", str(ctx.exception))
|
|
181
|
+
self.assertIn("SOBJECT", str(ctx.exception))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
unittest.main()
|