This is page 25 of 43. Use http://codebase.md/taurgis/sfcc-dev-mcp?page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .github │ ├── dependabot.yml │ ├── instructions │ │ ├── mcp-node-tests.instructions.md │ │ └── mcp-yml-tests.instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bug_fix.md │ │ ├── documentation.md │ │ └── new_tool.md │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── deploy-pages.yml │ ├── publish.yml │ └── update-docs.yml ├── .gitignore ├── .husky │ └── pre-commit ├── aegis.config.docs-only.json ├── aegis.config.json ├── aegis.config.with-dw.json ├── AGENTS.md ├── ai-instructions │ ├── claude-desktop │ │ └── claude_custom_instructions.md │ ├── cursor │ │ └── .cursor │ │ └── rules │ │ ├── debugging-workflows.mdc │ │ ├── hooks-development.mdc │ │ ├── isml-templates.mdc │ │ ├── job-framework.mdc │ │ ├── performance-optimization.mdc │ │ ├── scapi-endpoints.mdc │ │ ├── security-patterns.mdc │ │ ├── sfcc-development.mdc │ │ ├── sfra-controllers.mdc │ │ ├── sfra-models.mdc │ │ ├── system-objects.mdc │ │ └── testing-patterns.mdc │ └── github-copilot │ └── copilot-instructions.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── docs │ ├── best-practices │ │ ├── cartridge_creation.md │ │ ├── isml_templates.md │ │ ├── job_framework.md │ │ ├── localserviceregistry.md │ │ ├── ocapi_hooks.md │ │ ├── performance.md │ │ ├── scapi_custom_endpoint.md │ │ ├── scapi_hooks.md │ │ ├── security.md │ │ ├── sfra_client_side_js.md │ │ ├── sfra_controllers.md │ │ ├── sfra_models.md │ │ └── sfra_scss.md │ ├── dw_campaign │ │ ├── ABTest.md │ │ ├── ABTestMgr.md │ │ ├── ABTestSegment.md │ │ ├── AmountDiscount.md │ │ ├── ApproachingDiscount.md │ │ ├── BonusChoiceDiscount.md │ │ ├── BonusDiscount.md │ │ ├── Campaign.md │ │ ├── CampaignMgr.md │ │ ├── CampaignStatusCodes.md │ │ ├── Coupon.md │ │ ├── CouponMgr.md │ │ ├── CouponRedemption.md │ │ ├── CouponStatusCodes.md │ │ ├── Discount.md │ │ ├── DiscountPlan.md │ │ ├── FixedPriceDiscount.md │ │ ├── FixedPriceShippingDiscount.md │ │ ├── FreeDiscount.md │ │ ├── FreeShippingDiscount.md │ │ ├── PercentageDiscount.md │ │ ├── PercentageOptionDiscount.md │ │ ├── PriceBookPriceDiscount.md │ │ ├── Promotion.md │ │ ├── PromotionMgr.md │ │ ├── PromotionPlan.md │ │ ├── SlotContent.md │ │ ├── SourceCodeGroup.md │ │ ├── SourceCodeInfo.md │ │ ├── SourceCodeStatusCodes.md │ │ └── TotalFixedPriceDiscount.md │ ├── dw_catalog │ │ ├── Catalog.md │ │ ├── CatalogMgr.md │ │ ├── Category.md │ │ ├── CategoryAssignment.md │ │ ├── CategoryLink.md │ │ ├── PriceBook.md │ │ ├── PriceBookMgr.md │ │ ├── Product.md │ │ ├── ProductActiveData.md │ │ ├── ProductAttributeModel.md │ │ ├── ProductAvailabilityLevels.md │ │ ├── ProductAvailabilityModel.md │ │ ├── ProductInventoryList.md │ │ ├── ProductInventoryMgr.md │ │ ├── ProductInventoryRecord.md │ │ ├── ProductLink.md │ │ ├── ProductMgr.md │ │ ├── ProductOption.md │ │ ├── ProductOptionModel.md │ │ ├── ProductOptionValue.md │ │ ├── ProductPriceInfo.md │ │ ├── ProductPriceModel.md │ │ ├── ProductPriceTable.md │ │ ├── ProductSearchHit.md │ │ ├── ProductSearchModel.md │ │ ├── ProductSearchRefinementDefinition.md │ │ ├── ProductSearchRefinements.md │ │ ├── ProductSearchRefinementValue.md │ │ ├── ProductVariationAttribute.md │ │ ├── ProductVariationAttributeValue.md │ │ ├── ProductVariationModel.md │ │ ├── Recommendation.md │ │ ├── SearchModel.md │ │ ├── SearchRefinementDefinition.md │ │ ├── SearchRefinements.md │ │ ├── SearchRefinementValue.md │ │ ├── SortingOption.md │ │ ├── SortingRule.md │ │ ├── Store.md │ │ ├── StoreGroup.md │ │ ├── StoreInventoryFilter.md │ │ ├── StoreInventoryFilterValue.md │ │ ├── StoreMgr.md │ │ ├── Variant.md │ │ └── VariationGroup.md │ ├── dw_content │ │ ├── Content.md │ │ ├── ContentMgr.md │ │ ├── ContentSearchModel.md │ │ ├── ContentSearchRefinementDefinition.md │ │ ├── ContentSearchRefinements.md │ │ ├── ContentSearchRefinementValue.md │ │ ├── Folder.md │ │ ├── Library.md │ │ ├── MarkupText.md │ │ └── MediaFile.md │ ├── dw_crypto │ │ ├── CertificateRef.md │ │ ├── CertificateUtils.md │ │ ├── Cipher.md │ │ ├── Encoding.md │ │ ├── JWE.md │ │ ├── JWEHeader.md │ │ ├── JWS.md │ │ ├── JWSHeader.md │ │ ├── KeyRef.md │ │ ├── Mac.md │ │ ├── MessageDigest.md │ │ ├── SecureRandom.md │ │ ├── Signature.md │ │ ├── WeakCipher.md │ │ ├── WeakMac.md │ │ ├── WeakMessageDigest.md │ │ ├── WeakSignature.md │ │ └── X509Certificate.md │ ├── dw_customer │ │ ├── AddressBook.md │ │ ├── AgentUserMgr.md │ │ ├── AgentUserStatusCodes.md │ │ ├── AuthenticationStatus.md │ │ ├── Credentials.md │ │ ├── Customer.md │ │ ├── CustomerActiveData.md │ │ ├── CustomerAddress.md │ │ ├── CustomerCDPData.md │ │ ├── CustomerContextMgr.md │ │ ├── CustomerGroup.md │ │ ├── CustomerList.md │ │ ├── CustomerMgr.md │ │ ├── CustomerPasswordConstraints.md │ │ ├── CustomerPaymentInstrument.md │ │ ├── CustomerStatusCodes.md │ │ ├── EncryptedObject.md │ │ ├── ExternalProfile.md │ │ ├── OrderHistory.md │ │ ├── ProductList.md │ │ ├── ProductListItem.md │ │ ├── ProductListItemPurchase.md │ │ ├── ProductListMgr.md │ │ ├── ProductListRegistrant.md │ │ ├── Profile.md │ │ └── Wallet.md │ ├── dw_extensions.applepay │ │ ├── ApplePayHookResult.md │ │ └── ApplePayHooks.md │ ├── dw_extensions.facebook │ │ ├── FacebookFeedHooks.md │ │ └── FacebookProduct.md │ ├── dw_extensions.paymentrequest │ │ ├── PaymentRequestHookResult.md │ │ └── PaymentRequestHooks.md │ ├── dw_extensions.payments │ │ ├── SalesforceBancontactPaymentDetails.md │ │ ├── SalesforceCardPaymentDetails.md │ │ ├── SalesforceEpsPaymentDetails.md │ │ ├── SalesforceIdealPaymentDetails.md │ │ ├── SalesforceKlarnaPaymentDetails.md │ │ ├── SalesforcePaymentDetails.md │ │ ├── SalesforcePaymentIntent.md │ │ ├── SalesforcePaymentMethod.md │ │ ├── SalesforcePaymentRequest.md │ │ ├── SalesforcePaymentsHooks.md │ │ ├── SalesforcePaymentsMgr.md │ │ ├── SalesforcePaymentsSiteConfiguration.md │ │ ├── SalesforcePayPalOrder.md │ │ ├── SalesforcePayPalOrderAddress.md │ │ ├── SalesforcePayPalOrderPayer.md │ │ ├── SalesforcePayPalPaymentDetails.md │ │ ├── SalesforceSepaDebitPaymentDetails.md │ │ └── SalesforceVenmoPaymentDetails.md │ ├── dw_extensions.pinterest │ │ ├── PinterestAvailability.md │ │ ├── PinterestFeedHooks.md │ │ ├── PinterestOrder.md │ │ ├── PinterestOrderHooks.md │ │ └── PinterestProduct.md │ ├── dw_io │ │ ├── CSVStreamReader.md │ │ ├── CSVStreamWriter.md │ │ ├── File.md │ │ ├── FileReader.md │ │ ├── FileWriter.md │ │ ├── InputStream.md │ │ ├── OutputStream.md │ │ ├── PrintWriter.md │ │ ├── RandomAccessFileReader.md │ │ ├── Reader.md │ │ ├── StringWriter.md │ │ ├── Writer.md │ │ ├── XMLIndentingStreamWriter.md │ │ ├── XMLStreamConstants.md │ │ ├── XMLStreamReader.md │ │ └── XMLStreamWriter.md │ ├── dw_job │ │ ├── JobExecution.md │ │ └── JobStepExecution.md │ ├── dw_net │ │ ├── FTPClient.md │ │ ├── FTPFileInfo.md │ │ ├── HTTPClient.md │ │ ├── HTTPRequestPart.md │ │ ├── Mail.md │ │ ├── SFTPClient.md │ │ ├── SFTPFileInfo.md │ │ ├── WebDAVClient.md │ │ └── WebDAVFileInfo.md │ ├── dw_object │ │ ├── ActiveData.md │ │ ├── CustomAttributes.md │ │ ├── CustomObject.md │ │ ├── CustomObjectMgr.md │ │ ├── Extensible.md │ │ ├── ExtensibleObject.md │ │ ├── Note.md │ │ ├── ObjectAttributeDefinition.md │ │ ├── ObjectAttributeGroup.md │ │ ├── ObjectAttributeValueDefinition.md │ │ ├── ObjectTypeDefinition.md │ │ ├── PersistentObject.md │ │ ├── SimpleExtensible.md │ │ └── SystemObjectMgr.md │ ├── dw_order │ │ ├── AbstractItem.md │ │ ├── AbstractItemCtnr.md │ │ ├── Appeasement.md │ │ ├── AppeasementItem.md │ │ ├── Basket.md │ │ ├── BasketMgr.md │ │ ├── BonusDiscountLineItem.md │ │ ├── CouponLineItem.md │ │ ├── CreateAgentBasketLimitExceededException.md │ │ ├── CreateBasketFromOrderException.md │ │ ├── CreateCouponLineItemException.md │ │ ├── CreateOrderException.md │ │ ├── CreateTemporaryBasketLimitExceededException.md │ │ ├── GiftCertificate.md │ │ ├── GiftCertificateLineItem.md │ │ ├── GiftCertificateMgr.md │ │ ├── GiftCertificateStatusCodes.md │ │ ├── Invoice.md │ │ ├── InvoiceItem.md │ │ ├── LineItem.md │ │ ├── LineItemCtnr.md │ │ ├── Order.md │ │ ├── OrderAddress.md │ │ ├── OrderItem.md │ │ ├── OrderMgr.md │ │ ├── OrderPaymentInstrument.md │ │ ├── OrderProcessStatusCodes.md │ │ ├── PaymentCard.md │ │ ├── PaymentInstrument.md │ │ ├── PaymentMethod.md │ │ ├── PaymentMgr.md │ │ ├── PaymentProcessor.md │ │ ├── PaymentStatusCodes.md │ │ ├── PaymentTransaction.md │ │ ├── PriceAdjustment.md │ │ ├── PriceAdjustmentLimitTypes.md │ │ ├── ProductLineItem.md │ │ ├── ProductShippingCost.md │ │ ├── ProductShippingLineItem.md │ │ ├── ProductShippingModel.md │ │ ├── Return.md │ │ ├── ReturnCase.md │ │ ├── ReturnCaseItem.md │ │ ├── ReturnItem.md │ │ ├── Shipment.md │ │ ├── ShipmentShippingCost.md │ │ ├── ShipmentShippingModel.md │ │ ├── ShippingLineItem.md │ │ ├── ShippingLocation.md │ │ ├── ShippingMethod.md │ │ ├── ShippingMgr.md │ │ ├── ShippingOrder.md │ │ ├── ShippingOrderItem.md │ │ ├── SumItem.md │ │ ├── TaxGroup.md │ │ ├── TaxItem.md │ │ ├── TaxMgr.md │ │ ├── TrackingInfo.md │ │ └── TrackingRef.md │ ├── dw_order.hooks │ │ ├── CalculateHooks.md │ │ ├── OrderHooks.md │ │ ├── PaymentHooks.md │ │ ├── ReturnHooks.md │ │ └── ShippingOrderHooks.md │ ├── dw_rpc │ │ ├── SOAPUtil.md │ │ ├── Stub.md │ │ └── WebReference.md │ ├── dw_suggest │ │ ├── BrandSuggestions.md │ │ ├── CategorySuggestions.md │ │ ├── ContentSuggestions.md │ │ ├── CustomSuggestions.md │ │ ├── ProductSuggestions.md │ │ ├── SearchPhraseSuggestions.md │ │ ├── SuggestedCategory.md │ │ ├── SuggestedContent.md │ │ ├── SuggestedPhrase.md │ │ ├── SuggestedProduct.md │ │ ├── SuggestedTerm.md │ │ ├── SuggestedTerms.md │ │ ├── Suggestions.md │ │ └── SuggestModel.md │ ├── dw_svc │ │ ├── FTPService.md │ │ ├── FTPServiceDefinition.md │ │ ├── HTTPFormService.md │ │ ├── HTTPFormServiceDefinition.md │ │ ├── HTTPService.md │ │ ├── HTTPServiceDefinition.md │ │ ├── LocalServiceRegistry.md │ │ ├── Result.md │ │ ├── Service.md │ │ ├── ServiceCallback.md │ │ ├── ServiceConfig.md │ │ ├── ServiceCredential.md │ │ ├── ServiceDefinition.md │ │ ├── ServiceProfile.md │ │ ├── ServiceRegistry.md │ │ ├── SOAPService.md │ │ └── SOAPServiceDefinition.md │ ├── dw_system │ │ ├── AgentUserStatusCodes.md │ │ ├── Cache.md │ │ ├── CacheMgr.md │ │ ├── HookMgr.md │ │ ├── InternalObject.md │ │ ├── JobProcessMonitor.md │ │ ├── Log.md │ │ ├── Logger.md │ │ ├── LogNDC.md │ │ ├── OrganizationPreferences.md │ │ ├── Pipeline.md │ │ ├── PipelineDictionary.md │ │ ├── RemoteInclude.md │ │ ├── Request.md │ │ ├── RequestHooks.md │ │ ├── Response.md │ │ ├── RESTErrorResponse.md │ │ ├── RESTResponseMgr.md │ │ ├── RESTSuccessResponse.md │ │ ├── SearchStatus.md │ │ ├── Session.md │ │ ├── Site.md │ │ ├── SitePreferences.md │ │ ├── Status.md │ │ ├── StatusItem.md │ │ ├── System.md │ │ └── Transaction.md │ ├── dw_util │ │ ├── ArrayList.md │ │ ├── Assert.md │ │ ├── BigInteger.md │ │ ├── Bytes.md │ │ ├── Calendar.md │ │ ├── Collection.md │ │ ├── Currency.md │ │ ├── DateUtils.md │ │ ├── Decimal.md │ │ ├── FilteringCollection.md │ │ ├── Geolocation.md │ │ ├── HashMap.md │ │ ├── HashSet.md │ │ ├── Iterator.md │ │ ├── LinkedHashMap.md │ │ ├── LinkedHashSet.md │ │ ├── List.md │ │ ├── Locale.md │ │ ├── Map.md │ │ ├── MapEntry.md │ │ ├── MappingKey.md │ │ ├── MappingMgr.md │ │ ├── PropertyComparator.md │ │ ├── SecureEncoder.md │ │ ├── SecureFilter.md │ │ ├── SeekableIterator.md │ │ ├── Set.md │ │ ├── SortedMap.md │ │ ├── SortedSet.md │ │ ├── StringUtils.md │ │ ├── Template.md │ │ └── UUIDUtils.md │ ├── dw_value │ │ ├── EnumValue.md │ │ ├── MimeEncodedText.md │ │ ├── Money.md │ │ └── Quantity.md │ ├── dw_web │ │ ├── ClickStream.md │ │ ├── ClickStreamEntry.md │ │ ├── Cookie.md │ │ ├── Cookies.md │ │ ├── CSRFProtection.md │ │ ├── Form.md │ │ ├── FormAction.md │ │ ├── FormElement.md │ │ ├── FormElementValidationResult.md │ │ ├── FormField.md │ │ ├── FormFieldOption.md │ │ ├── FormFieldOptions.md │ │ ├── FormGroup.md │ │ ├── FormList.md │ │ ├── FormListItem.md │ │ ├── Forms.md │ │ ├── HttpParameter.md │ │ ├── HttpParameterMap.md │ │ ├── LoopIterator.md │ │ ├── PageMetaData.md │ │ ├── PageMetaTag.md │ │ ├── PagingModel.md │ │ ├── Resource.md │ │ ├── URL.md │ │ ├── URLAction.md │ │ ├── URLParameter.md │ │ ├── URLRedirect.md │ │ ├── URLRedirectMgr.md │ │ └── URLUtils.md │ ├── sfra │ │ ├── account.md │ │ ├── address.md │ │ ├── billing.md │ │ ├── cart.md │ │ ├── categories.md │ │ ├── content.md │ │ ├── locale.md │ │ ├── order.md │ │ ├── payment.md │ │ ├── price-default.md │ │ ├── price-range.md │ │ ├── price-tiered.md │ │ ├── product-bundle.md │ │ ├── product-full.md │ │ ├── product-line-items.md │ │ ├── product-search.md │ │ ├── product-tile.md │ │ ├── querystring.md │ │ ├── render.md │ │ ├── request.md │ │ ├── response.md │ │ ├── server.md │ │ ├── shipping.md │ │ ├── store.md │ │ ├── stores.md │ │ └── totals.md │ └── TopLevel │ ├── APIException.md │ ├── arguments.md │ ├── Array.md │ ├── ArrayBuffer.md │ ├── BigInt.md │ ├── Boolean.md │ ├── ConversionError.md │ ├── DataView.md │ ├── Date.md │ ├── Error.md │ ├── ES6Iterator.md │ ├── EvalError.md │ ├── Fault.md │ ├── Float32Array.md │ ├── Float64Array.md │ ├── Function.md │ ├── Generator.md │ ├── global.md │ ├── Int16Array.md │ ├── Int32Array.md │ ├── Int8Array.md │ ├── InternalError.md │ ├── IOError.md │ ├── Iterable.md │ ├── Iterator.md │ ├── JSON.md │ ├── Map.md │ ├── Math.md │ ├── Module.md │ ├── Namespace.md │ ├── Number.md │ ├── Object.md │ ├── QName.md │ ├── RangeError.md │ ├── ReferenceError.md │ ├── RegExp.md │ ├── Set.md │ ├── StopIteration.md │ ├── String.md │ ├── Symbol.md │ ├── SyntaxError.md │ ├── SystemError.md │ ├── TypeError.md │ ├── Uint16Array.md │ ├── Uint32Array.md │ ├── Uint8Array.md │ ├── Uint8ClampedArray.md │ ├── URIError.md │ ├── WeakMap.md │ ├── WeakSet.md │ ├── XML.md │ ├── XMLList.md │ └── XMLStreamError.md ├── docs-site │ ├── .gitignore │ ├── App.tsx │ ├── components │ │ ├── Badge.tsx │ │ ├── BreadcrumbSchema.tsx │ │ ├── CodeBlock.tsx │ │ ├── Collapsible.tsx │ │ ├── ConfigBuilder.tsx │ │ ├── ConfigHero.tsx │ │ ├── ConfigModeTabs.tsx │ │ ├── icons.tsx │ │ ├── Layout.tsx │ │ ├── LightCodeContainer.tsx │ │ ├── NewcomerCTA.tsx │ │ ├── NextStepsStrip.tsx │ │ ├── OnThisPage.tsx │ │ ├── Search.tsx │ │ ├── SEO.tsx │ │ ├── Sidebar.tsx │ │ ├── StructuredData.tsx │ │ ├── ToolCard.tsx │ │ ├── ToolFilters.tsx │ │ ├── Typography.tsx │ │ └── VersionBadge.tsx │ ├── constants.tsx │ ├── index.html │ ├── main.tsx │ ├── metadata.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── AIInterfacesPage.tsx │ │ ├── ConfigurationPage.tsx │ │ ├── DevelopmentPage.tsx │ │ ├── ExamplesPage.tsx │ │ ├── FeaturesPage.tsx │ │ ├── HomePage.tsx │ │ ├── SecurityPage.tsx │ │ ├── ToolsPage.tsx │ │ └── TroubleshootingPage.tsx │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── security.txt │ │ ├── 404.html │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── explain-product-pricing-methods-no-mcp.png │ │ ├── explain-product-pricing-methods.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── llms.txt │ │ ├── robots.txt │ │ ├── site.webmanifest │ │ └── sitemap.xml │ ├── README.md │ ├── scripts │ │ ├── generate-search-index.js │ │ ├── generate-sitemap.js │ │ └── search-dev.js │ ├── src │ │ └── styles │ │ ├── input.css │ │ └── prism-theme.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types.ts │ ├── utils │ │ ├── search.ts │ │ └── toolsData.ts │ └── vite.config.ts ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── convert-docs.js ├── SECURITY.md ├── server.json ├── src │ ├── clients │ │ ├── base │ │ │ ├── http-client.ts │ │ │ ├── oauth-token.ts │ │ │ └── ocapi-auth-client.ts │ │ ├── best-practices-client.ts │ │ ├── cartridge-generation-client.ts │ │ ├── docs │ │ │ ├── class-content-parser.ts │ │ │ ├── class-name-resolver.ts │ │ │ ├── documentation-scanner.ts │ │ │ ├── index.ts │ │ │ └── referenced-types-extractor.ts │ │ ├── docs-client.ts │ │ ├── log-client.ts │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── log-analyzer.ts │ │ │ ├── log-client.ts │ │ │ ├── log-constants.ts │ │ │ ├── log-file-discovery.ts │ │ │ ├── log-file-reader.ts │ │ │ ├── log-formatter.ts │ │ │ ├── log-processor.ts │ │ │ ├── log-types.ts │ │ │ └── webdav-client-manager.ts │ │ ├── ocapi │ │ │ ├── code-versions-client.ts │ │ │ ├── site-preferences-client.ts │ │ │ └── system-objects-client.ts │ │ ├── ocapi-client.ts │ │ └── sfra-client.ts │ ├── config │ │ ├── configuration-factory.ts │ │ └── dw-json-loader.ts │ ├── core │ │ ├── handlers │ │ │ ├── abstract-log-tool-handler.ts │ │ │ ├── base-handler.ts │ │ │ ├── best-practices-handler.ts │ │ │ ├── cartridge-handler.ts │ │ │ ├── client-factory.ts │ │ │ ├── code-version-handler.ts │ │ │ ├── docs-handler.ts │ │ │ ├── job-log-handler.ts │ │ │ ├── job-log-tool-config.ts │ │ │ ├── log-handler.ts │ │ │ ├── log-tool-config.ts │ │ │ ├── sfra-handler.ts │ │ │ ├── system-object-handler.ts │ │ │ └── validation-helpers.ts │ │ ├── server.ts │ │ └── tool-definitions.ts │ ├── index.ts │ ├── main.ts │ ├── services │ │ ├── file-system-service.ts │ │ ├── index.ts │ │ └── path-service.ts │ ├── tool-configs │ │ ├── best-practices-tool-config.ts │ │ ├── cartridge-tool-config.ts │ │ ├── code-version-tool-config.ts │ │ ├── docs-tool-config.ts │ │ ├── job-log-tool-config.ts │ │ ├── log-tool-config.ts │ │ ├── sfra-tool-config.ts │ │ └── system-object-tool-config.ts │ ├── types │ │ └── types.ts │ └── utils │ ├── cache.ts │ ├── job-log-tool-config.ts │ ├── job-log-utils.ts │ ├── log-cache.ts │ ├── log-tool-config.ts │ ├── log-tool-constants.ts │ ├── log-tool-utils.ts │ ├── logger.ts │ ├── ocapi-url-builder.ts │ ├── path-resolver.ts │ ├── query-builder.ts │ ├── utils.ts │ └── validator.ts ├── tests │ ├── __mocks__ │ │ ├── docs-client.ts │ │ ├── src │ │ │ └── clients │ │ │ └── base │ │ │ └── http-client.js │ │ └── webdav.js │ ├── base-handler.test.ts │ ├── base-http-client.test.ts │ ├── best-practices-handler.test.ts │ ├── cache.test.ts │ ├── cartridge-handler.test.ts │ ├── class-content-parser.test.ts │ ├── class-name-resolver.test.ts │ ├── client-factory.test.ts │ ├── code-version-handler.test.ts │ ├── code-versions-client.test.ts │ ├── config.test.ts │ ├── configuration-factory.test.ts │ ├── docs-handler.test.ts │ ├── documentation-scanner.test.ts │ ├── file-system-service.test.ts │ ├── job-log-handler.test.ts │ ├── job-log-utils.test.ts │ ├── log-client.test.ts │ ├── log-handler.test.ts │ ├── log-processor.test.ts │ ├── logger.test.ts │ ├── mcp │ │ ├── AGENTS.md │ │ ├── node │ │ │ ├── activate-code-version-advanced.full-mode.programmatic.test.js │ │ │ ├── code-versions.full-mode.programmatic.test.js │ │ │ ├── generate-cartridge-structure.docs-only.programmatic.test.js │ │ │ ├── get-available-best-practice-guides.docs-only.programmatic.test.js │ │ │ ├── get-available-sfra-documents.programmatic.test.js │ │ │ ├── get-best-practice-guide.docs-only.programmatic.test.js │ │ │ ├── get-hook-reference.docs-only.programmatic.test.js │ │ │ ├── get-job-execution-summary.full-mode.programmatic.test.js │ │ │ ├── get-job-log-entries.full-mode.programmatic.test.js │ │ │ ├── get-latest-debug.full-mode.programmatic.test.js │ │ │ ├── get-latest-error.full-mode.programmatic.test.js │ │ │ ├── get-latest-info.full-mode.programmatic.test.js │ │ │ ├── get-latest-job-log-files.full-mode.programmatic.test.js │ │ │ ├── get-latest-warn.full-mode.programmatic.test.js │ │ │ ├── get-log-file-contents.full-mode.programmatic.test.js │ │ │ ├── get-sfcc-class-documentation.docs-only.programmatic.test.js │ │ │ ├── get-sfcc-class-info.docs-only.programmatic.test.js │ │ │ ├── get-sfra-categories.docs-only.programmatic.test.js │ │ │ ├── get-sfra-document.programmatic.test.js │ │ │ ├── get-sfra-documents-by-category.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definition.full-mode.programmatic.test.js │ │ │ ├── get-system-object-definitions.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definitions.full-mode.programmatic.test.js │ │ │ ├── list-log-files.full-mode.programmatic.test.js │ │ │ ├── list-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-best-practices.docs-only.programmatic.test.js │ │ │ ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-job-logs-by-name.full-mode.programmatic.test.js │ │ │ ├── search-job-logs.full-mode.programmatic.test.js │ │ │ ├── search-logs.full-mode.programmatic.test.js │ │ │ ├── search-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-sfcc-methods.docs-only.programmatic.test.js │ │ │ ├── search-sfra-documentation.docs-only.programmatic.test.js │ │ │ ├── search-site-preferences.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-groups.full-mode.programmatic.test.js │ │ │ ├── summarize-logs.full-mode.programmatic.test.js │ │ │ ├── tools.docs-only.programmatic.test.js │ │ │ └── tools.full-mode.programmatic.test.js │ │ ├── README.md │ │ ├── test-fixtures │ │ │ └── dw.json │ │ └── yaml │ │ ├── activate-code-version.docs-only.test.mcp.yml │ │ ├── activate-code-version.full-mode.test.mcp.yml │ │ ├── get_latest_error.test.mcp.yml │ │ ├── get-available-best-practice-guides.docs-only.test.mcp.yml │ │ ├── get-available-best-practice-guides.full-mode.test.mcp.yml │ │ ├── get-available-sfra-documents.docs-only.test.mcp.yml │ │ ├── get-available-sfra-documents.full-mode.test.mcp.yml │ │ ├── get-best-practice-guide.docs-only.test.mcp.yml │ │ ├── get-best-practice-guide.full-mode.test.mcp.yml │ │ ├── get-code-versions.docs-only.test.mcp.yml │ │ ├── get-code-versions.full-mode.test.mcp.yml │ │ ├── get-hook-reference.docs-only.test.mcp.yml │ │ ├── get-hook-reference.full-mode.test.mcp.yml │ │ ├── get-job-execution-summary.full-mode.test.mcp.yml │ │ ├── get-job-log-entries.full-mode.test.mcp.yml │ │ ├── get-latest-debug.full-mode.test.mcp.yml │ │ ├── get-latest-error.full-mode.test.mcp.yml │ │ ├── get-latest-info.full-mode.test.mcp.yml │ │ ├── get-latest-job-log-files.full-mode.test.mcp.yml │ │ ├── get-latest-warn.full-mode.test.mcp.yml │ │ ├── get-log-file-contents.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-documentation.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-documentation.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-info.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-info.full-mode.test.mcp.yml │ │ ├── get-sfra-categories.docs-only.test.mcp.yml │ │ ├── get-sfra-categories.full-mode.test.mcp.yml │ │ ├── get-sfra-document.docs-only.test.mcp.yml │ │ ├── get-sfra-document.full-mode.test.mcp.yml │ │ ├── get-sfra-documents-by-category.docs-only.test.mcp.yml │ │ ├── get-sfra-documents-by-category.full-mode.test.mcp.yml │ │ ├── get-system-object-definition.docs-only.test.mcp.yml │ │ ├── get-system-object-definition.full-mode.test.mcp.yml │ │ ├── get-system-object-definitions.docs-only.test.mcp.yml │ │ ├── get-system-object-definitions.full-mode.test.mcp.yml │ │ ├── list-log-files.full-mode.test.mcp.yml │ │ ├── list-sfcc-classes.docs-only.test.mcp.yml │ │ ├── list-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-best-practices.docs-only.test.mcp.yml │ │ ├── search-best-practices.full-mode.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.test.mcp.yml │ │ ├── search-job-logs-by-name.full-mode.test.mcp.yml │ │ ├── search-job-logs.full-mode.test.mcp.yml │ │ ├── search-logs.full-mode.test.mcp.yml │ │ ├── search-sfcc-classes.docs-only.test.mcp.yml │ │ ├── search-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-sfcc-methods.docs-only.test.mcp.yml │ │ ├── search-sfcc-methods.full-mode.test.mcp.yml │ │ ├── search-sfra-documentation.docs-only.test.mcp.yml │ │ ├── search-sfra-documentation.full-mode.test.mcp.yml │ │ ├── search-site-preferences.docs-only.test.mcp.yml │ │ ├── search-site-preferences.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-groups.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-groups.full-mode.test.mcp.yml │ │ ├── summarize-logs.full-mode.test.mcp.yml │ │ ├── tools.docs-only.test.mcp.yml │ │ └── tools.full-mode.test.mcp.yml │ ├── oauth-token.test.ts │ ├── ocapi-auth-client.test.ts │ ├── ocapi-client.test.ts │ ├── path-service.test.ts │ ├── query-builder.test.ts │ ├── referenced-types-extractor.test.ts │ ├── servers │ │ ├── sfcc-mock-server │ │ │ ├── mock-data │ │ │ │ └── ocapi │ │ │ │ ├── code-versions.json │ │ │ │ ├── custom-object-attributes-customapi.json │ │ │ │ ├── custom-object-attributes-globalsettings.json │ │ │ │ ├── custom-object-attributes-versionhistory.json │ │ │ │ ├── site-preferences-ccv.json │ │ │ │ ├── site-preferences-fastforward.json │ │ │ │ ├── site-preferences-sfra.json │ │ │ │ ├── site-preferences-storefront.json │ │ │ │ ├── site-preferences-system.json │ │ │ │ ├── system-object-attribute-groups-campaign.json │ │ │ │ ├── system-object-attribute-groups-category.json │ │ │ │ ├── system-object-attribute-groups-order.json │ │ │ │ ├── system-object-attribute-groups-product.json │ │ │ │ ├── system-object-attribute-groups-sitepreferences.json │ │ │ │ ├── system-object-attributes-customeraddress.json │ │ │ │ ├── system-object-attributes-product-expanded.json │ │ │ │ ├── system-object-attributes-product.json │ │ │ │ ├── system-object-definition-category.json │ │ │ │ ├── system-object-definition-customer.json │ │ │ │ ├── system-object-definition-customeraddress.json │ │ │ │ ├── system-object-definition-order.json │ │ │ │ ├── system-object-definition-product.json │ │ │ │ ├── system-object-definitions-old.json │ │ │ │ └── system-object-definitions.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ ├── scripts │ │ │ │ └── setup-logs.js │ │ │ ├── server.js │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── server-config.js │ │ │ ├── middleware │ │ │ │ ├── auth.js │ │ │ │ ├── cors.js │ │ │ │ └── logging.js │ │ │ ├── routes │ │ │ │ ├── ocapi │ │ │ │ │ ├── code-versions-handler.js │ │ │ │ │ ├── oauth-handler.js │ │ │ │ │ ├── ocapi-error-utils.js │ │ │ │ │ ├── ocapi-utils.js │ │ │ │ │ ├── site-preferences-handler.js │ │ │ │ │ └── system-objects-handler.js │ │ │ │ ├── ocapi.js │ │ │ │ └── webdav.js │ │ │ └── utils │ │ │ ├── mock-data-loader.js │ │ │ └── webdav-xml.js │ │ └── sfcc-mock-server-manager.ts │ ├── sfcc-mock-server.test.ts │ ├── site-preferences-client.test.ts │ ├── system-objects-client.test.ts │ ├── utils.test.ts │ ├── validation-helpers.test.ts │ └── validator.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /tests/mcp/node/get-latest-warn.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript import { test, describe, before, after, beforeEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { connect } from 'mcp-aegis'; describe('get_latest_warn - Full Mode Programmatic Tests', () => { let client; before(async () => { client = await connect('./aegis.config.with-dw.json'); }); after(async () => { if (client?.connected) { await client.disconnect(); } }); beforeEach(() => { // CRITICAL: Clear all buffers to prevent leaking into next tests client.clearAllBuffers(); // Recommended - comprehensive protection }); // Helper functions for common validations function assertValidMCPResponse(result) { assert.ok(result.content, 'Should have content'); assert.ok(Array.isArray(result.content), 'Content should be array'); assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); } function assertErrorResponse(result, expectedErrorText) { assertValidMCPResponse(result); assert.equal(result.isError, true, 'Should be an error response'); assert.equal(result.content[0].type, 'text'); assert.ok(result.content[0].text.includes(expectedErrorText), `Expected error text "${expectedErrorText}" in "${result.content[0].text}"`); } function assertSuccessResponse(result) { assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should not be an error response'); assert.equal(result.content[0].type, 'text'); } function assertLogFormat(result, expectedLimit) { assertSuccessResponse(result); const text = result.content[0].text; // Should contain the expected limit message assert.ok(text.includes(`Latest ${expectedLimit} warn messages`), `Should mention "${expectedLimit}" warn messages`); // Should contain log file name pattern assert.ok(/warn-blade-\d{8}-\d{6}\.log/.test(text), 'Should contain warn log file name pattern'); // Should contain WARN level entries assert.ok(text.includes('WARN'), 'Should contain WARN level entries'); // Should contain GMT timestamps assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(text), 'Should contain GMT timestamp pattern'); } // Helper function to get current date in YYYYMMDD format function getCurrentDateString() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}${month}${day}`; } // Core functionality tests - focused on programmatic strengths describe('Core Functionality', () => { test('should retrieve latest warn messages with default parameters', async () => { const result = await client.callTool('get_latest_warn', {}); assertLogFormat(result, 10); // Default limit is 10 // Should contain SFCC-specific patterns const text = result.content[0].text; assert.ok(/PipelineCallServlet|SystemJobThread/.test(text), 'Should contain SFCC thread patterns'); assert.ok(text.includes('Sites-'), 'Should contain Sites information'); }); test('should handle comprehensive parameter combinations', async () => { const paramCombinations = [ { limit: 3 }, { date: getCurrentDateString(), limit: 2 }, { limit: 50 }, {} // default parameters ]; const results = []; // Execute calls sequentially (never use Promise.all with MCP!) for (const params of paramCombinations) { const result = await client.callTool('get_latest_warn', params); results.push({ params, result }); } // All should have consistent structure results.forEach(({ params, result }, index) => { assertValidMCPResponse(result); assert.equal(result.isError, false, `Call ${index} should not be error`); assert.equal(result.content[0].type, 'text', `Call ${index} should have text content`); const expectedLimit = params.limit || 10; assert.ok(result.content[0].text.includes(`Latest ${expectedLimit} warn messages`), `Call ${index} should contain 'Latest ${expectedLimit}' in response`); }); }); }); // Error handling and edge cases - key validation scenarios describe('Error Handling and Edge Cases', () => { test('should handle parameter validation errors correctly', async () => { const errorCases = [ { params: { limit: '5' }, expectedError: 'Invalid limit \'5\' for get_latest_warn. Must be a valid number' }, { params: { limit: 0 }, expectedError: 'Invalid limit \'0\' for get_latest_warn. Must be between 1 and 1000' }, { params: { limit: -5 }, expectedError: 'Invalid limit \'-5\'' }, { params: { limit: 9999 }, expectedError: 'Invalid limit' } ]; for (const { params, expectedError } of errorCases) { const result = await client.callTool('get_latest_warn', params); assertErrorResponse(result, expectedError); } }); test('should handle edge cases without breaking server state', async () => { const edgeCases = [ { date: '' }, // Empty date { date: '2024-01-01' }, // Invalid format { date: '20251231' }, // Future date { invalid: 'param' } // Invalid parameter name ]; // Test all edge cases sequentially for (const testCase of edgeCases) { const result = await client.callTool('get_latest_warn', testCase); assertValidMCPResponse(result); // Some may succeed with no data, some may have default behavior - but none should crash } // Verify tool still works after edge cases const finalResult = await client.callTool('get_latest_warn', { limit: 1 }); assertSuccessResponse(finalResult); }); }); // Advanced content analysis - leveraging programmatic strengths describe('SFCC Content Analysis', () => { test('should contain and analyze SFCC-specific warning patterns', async () => { const result = await client.callTool('get_latest_warn', { limit: 5 }); assertSuccessResponse(result); const text = result.content[0].text; // SFCC-specific validation patterns for warnings const sfccWarningPatterns = [ /Sites-\w+/, // Sites names /PipelineCallServlet|SystemJobThread/, // Thread types /\|\d+\|/, // Thread IDs /custom \[\]/, // Custom category /inventory low|Content asset|offline/i // Common warning types ]; const matchedPatterns = sfccWarningPatterns.filter(pattern => pattern.test(text)); assert.ok(matchedPatterns.length >= 2, `Should match at least 2 SFCC warning patterns. Matched: ${matchedPatterns.length}`); // Analyze log structure elements assert.ok(/warn-blade-.*\.log/.test(text), 'Should include log file name'); assert.ok(/\d{4}-\d{2}-\d{2}/.test(text), 'Should include timestamp'); assert.ok(/GMT/.test(text), 'Should include GMT timezone'); assert.ok(/WARN PipelineCallServlet/.test(text), 'Should include servlet context'); // Count separators - should have appropriate separators for multiple entries const separators = text.match(/---/g); assert.ok(separators && separators.length >= 1, `Should have separators for multiple entries. Found: ${separators?.length || 0}`); }); test('should validate warning content quality and context', async () => { const result = await client.callTool('get_latest_warn', { limit: 3 }); assertSuccessResponse(result); const text = result.content[0].text; const lines = text.split('\n').filter(line => line.trim().length > 0); // Dynamic validation based on actual content const warnLines = lines.filter(line => line.includes('WARN')); assert.ok(warnLines.length > 0, 'Should have at least one WARN line'); // Each warn line should have proper structure warnLines.forEach((line, index) => { assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(line), `Warn line ${index} should contain timestamp: ${line}`); }); // Warning-specific content analysis const warningIndicators = [ 'inventory low', 'Content asset', 'offline', 'deprecated' ]; const foundIndicators = warningIndicators.filter(indicator => text.toLowerCase().includes(indicator.toLowerCase()) ); assert.ok(foundIndicators.length > 0, `Should contain warning indicators. Found: ${foundIndicators.join(', ')}`); }); }); // Multi-step workflows - where programmatic tests excel describe('Multi-Step Workflows and State Management', () => { test('should support comprehensive warning analysis workflow', async () => { // Step 1: Get recent warnings with small limit const recentWarnings = await client.callTool('get_latest_warn', { limit: 2 }); assertSuccessResponse(recentWarnings); // Step 2: Get more comprehensive warning list const comprehensiveWarnings = await client.callTool('get_latest_warn', { limit: 10 }); assertSuccessResponse(comprehensiveWarnings); // Step 3: Get warnings for specific date const dateSpecificWarnings = await client.callTool('get_latest_warn', { date: getCurrentDateString(), limit: 5 }); assertSuccessResponse(dateSpecificWarnings); // Cross-analysis: Verify pattern consistency across different scopes const recentText = recentWarnings.content[0].text; const comprehensiveText = comprehensiveWarnings.content[0].text; const dateText = dateSpecificWarnings.content[0].text; [recentText, comprehensiveText, dateText].forEach((text, index) => { assert.ok(text.includes('WARN'), `Analysis ${index} should contain WARN level`); assert.ok(/Sites-/.test(text), `Analysis ${index} should contain Sites information`); assert.ok(/PipelineCallServlet/.test(text), `Analysis ${index} should contain servlet context`); }); // Advanced: Verify scope scaling const recentCount = (recentText.match(/WARN/g) || []).length; const comprehensiveCount = (comprehensiveText.match(/WARN/g) || []).length; assert.ok(comprehensiveCount >= recentCount, 'Comprehensive analysis should include at least as many warnings as recent analysis'); }); test('should maintain server state integrity across sequential operations', async () => { const operationSequence = [ { params: { limit: 1 }, description: 'Single warning check' }, { params: { limit: 20 }, description: 'Large batch analysis' }, { params: { limit: 0 }, description: 'Error condition', expectError: true }, { params: { limit: 5 }, description: 'Recovery operation' }, { params: { date: getCurrentDateString(), limit: 3 }, description: 'Date-specific analysis' } ]; const results = []; // Execute operations sequentially to test state management for (const operation of operationSequence) { const result = await client.callTool('get_latest_warn', operation.params); results.push({ operation: operation.description, params: operation.params, success: !result.isError, expectedError: operation.expectError || false, result }); } // Analyze operation results for state integrity results.forEach((opResult, index) => { if (opResult.expectedError) { assert.equal(opResult.success, false, `Operation ${index} (${opResult.operation}) should have failed as expected`); } else { assert.equal(opResult.success, true, `Operation ${index} (${opResult.operation}) should have succeeded`); // Verify state consistency for successful operations const text = opResult.result.content[0].text; const expectedLimit = opResult.params.limit || 10; assert.ok(text.includes(`Latest ${expectedLimit} warn messages`), `Operation ${index} should show correct limit: ${expectedLimit}`); } }); // Final verification: Server should still be fully functional const finalCheck = await client.callTool('get_latest_warn', { limit: 1 }); assertSuccessResponse(finalCheck); }); }); // Operational monitoring and resilience - complex business logic validation describe('Operational Monitoring and Resilience', () => { test('should provide comprehensive monitoring capabilities', async () => { // Simulate different monitoring scenarios const monitoringScenarios = [ { limit: 1, description: 'Latest warning check', alertLevel: 'immediate' }, { limit: 10, description: 'Recent warnings review', alertLevel: 'hourly' }, { limit: 50, description: 'Comprehensive warning analysis', alertLevel: 'daily' } ]; const monitoringResults = []; for (const scenario of monitoringScenarios) { const result = await client.callTool('get_latest_warn', { limit: scenario.limit }); const analysisResult = { scenario: scenario.description, alertLevel: scenario.alertLevel, limit: scenario.limit, success: !result.isError, warningCount: 0, hasWarnings: false, sfccPatterns: 0 }; if (!result.isError) { const text = result.content[0].text; analysisResult.hasWarnings = text.includes('WARN'); analysisResult.warningCount = (text.match(/WARN/g) || []).length; // Count SFCC-specific patterns for monitoring quality const sfccPatterns = [/Sites-/, /PipelineCallServlet/, /custom \[\]/]; analysisResult.sfccPatterns = sfccPatterns.filter(pattern => pattern.test(text)).length; } monitoringResults.push(analysisResult); } // Validate monitoring effectiveness monitoringResults.forEach(result => { assert.ok(result.success, `${result.scenario} should succeed`); assert.ok(result.hasWarnings, `${result.scenario} should contain warnings`); assert.ok(result.sfccPatterns >= 1, `${result.scenario} should contain SFCC patterns`); }); // Verify monitoring scope increases with limit assert.ok(monitoringResults[0].limit < monitoringResults[1].limit, 'Monitoring scope should increase'); assert.ok(monitoringResults[1].limit < monitoringResults[2].limit, 'Comprehensive analysis should have largest scope'); }); test('should handle resilience scenarios and error recovery', async () => { // Test normal operation baseline const baselineResult = await client.callTool('get_latest_warn', { limit: 3 }); assertSuccessResponse(baselineResult); // Test various failure scenarios and recovery const resilienceTests = [ { params: { limit: 0 }, shouldFail: true, description: 'Zero limit validation' }, { params: { limit: '1' }, shouldFail: true, description: 'Type validation' }, { params: { limit: 99999 }, shouldFail: true, description: 'Range validation' }, { params: { date: '' }, shouldFail: false, description: 'Empty date handling' }, { params: { invalid: 'param' }, shouldFail: false, description: 'Unknown parameter handling' } ]; let failureCount = 0; let recoveryCount = 0; for (const resilienceTest of resilienceTests) { const result = await client.callTool('get_latest_warn', resilienceTest.params); if (resilienceTest.shouldFail) { assert.equal(result.isError, true, `${resilienceTest.description} should fail as expected`); failureCount++; } else { assert.equal(result.isError, false, `${resilienceTest.description} should succeed`); } // Test recovery after each scenario const recoveryResult = await client.callTool('get_latest_warn', { limit: 1 }); assert.equal(recoveryResult.isError, false, `Recovery after ${resilienceTest.description} should work`); recoveryCount++; } // Verify resilience metrics assert.ok(failureCount >= 3, 'Should have tested multiple failure scenarios'); assert.equal(recoveryCount, resilienceTests.length, 'All recovery tests should pass'); // Final comprehensive recovery test const finalResult = await client.callTool('get_latest_warn', { limit: 5 }); assertSuccessResponse(finalResult); const finalText = finalResult.content[0].text; assert.ok(finalText.includes('Latest 5 warn messages'), 'Final recovery should work correctly'); assert.ok(finalText.includes('WARN'), 'Final recovery should contain valid warning data'); }); test('should provide detailed data quality analysis across different parameters', async () => { const qualityTests = [ { params: { limit: 2 }, minWarnings: 1, description: 'Small sample quality' }, { params: { limit: 10 }, minWarnings: 3, description: 'Standard sample quality' }, { params: { date: getCurrentDateString(), limit: 5 }, minWarnings: 1, description: 'Date-specific quality' } ]; const qualityResults = []; for (const qualityTest of qualityTests) { const result = await client.callTool('get_latest_warn', qualityTest.params); assertSuccessResponse(result); const text = result.content[0].text; // Quality metrics const qualityMetrics = { hasLogFileName: /warn-blade-.*\.log/.test(text), hasTimestamp: /\d{4}-\d{2}-\d{2}/.test(text), hasSiteInfo: /Sites-/.test(text), hasWarnLevel: text.includes('WARN'), hasGMTTimezone: text.includes('GMT'), warningCount: (text.match(/WARN/g) || []).length, separatorCount: (text.match(/---/g) || []).length }; qualityResults.push({ description: qualityTest.description, params: qualityTest.params, metrics: qualityMetrics, passedChecks: Object.values(qualityMetrics).filter(Boolean).length }); // Validate quality requirements assert.ok(qualityMetrics.hasLogFileName, `${qualityTest.description}: Should have log file name`); assert.ok(qualityMetrics.hasTimestamp, `${qualityTest.description}: Should have timestamp`); assert.ok(qualityMetrics.hasWarnLevel, `${qualityTest.description}: Should have WARN level`); assert.ok(qualityMetrics.warningCount >= qualityTest.minWarnings, `${qualityTest.description}: Should have at least ${qualityTest.minWarnings} warnings`); } // Cross-quality analysis const avgQualityScore = qualityResults.reduce((sum, result) => sum + result.passedChecks, 0) / qualityResults.length; assert.ok(avgQualityScore >= 6, `Average quality score should be high (${avgQualityScore})`); // Consistency check across different parameter sets qualityResults.forEach(result => { assert.ok(result.passedChecks >= 6, `${result.description} should pass most quality checks (${result.passedChecks})`); }); }); }); }); ``` -------------------------------------------------------------------------------- /docs/dw_util/StringUtils.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.util # Class StringUtils ## Inheritance Hierarchy - Object - dw.util.StringUtils ## Description String utility class. ## Constants ### ENCODE_TYPE_HTML **Type:** Number = 0 String encoding type HTML. ### ENCODE_TYPE_WML **Type:** Number = 2 String encoding type WML. ### ENCODE_TYPE_XML **Type:** Number = 1 String encoding type XML. ### TRUNCATE_CHAR **Type:** String = "char" String truncate mode 'char'. Truncate string to the nearest character. Default mode if no truncate mode is specified. ### TRUNCATE_SENTENCE **Type:** String = "sentence" String truncate mode 'sentence'. Truncate string to the nearest sentence. ### TRUNCATE_WORD **Type:** String = "word" String truncate mode 'word'. Truncate string to the nearest word. ## Properties ## Constructor Summary ## Method Summary ### decodeBase64 **Signature:** `static decodeBase64(base64 : String) : String` Interprets a Base64 encoded string as byte stream of an UTF-8 encoded string. ### decodeBase64 **Signature:** `static decodeBase64(base64 : String, characterEncoding : String) : String` Interprets a Base64 encoded string as the byte stream representation of a string. ### decodeString **Signature:** `static decodeString(str : String, type : Number) : String` Convert a given syntax-safe string to a string according to the selected character entity encoding type. ### encodeBase64 **Signature:** `static encodeBase64(str : String) : String` Encodes the byte representation of the given string as Base64. ### encodeBase64 **Signature:** `static encodeBase64(str : String, characterEncoding : String) : String` Encodes the byte representation of the given string as Base64. ### encodeString **Signature:** `static encodeString(str : String, type : Number) : String` Convert a given string to a syntax-safe string according to the selected character entity encoding type. ### format **Signature:** `static format(format : String, args : Object...) : String` Returns a formatted string using the specified format and arguments. ### formatCalendar **Signature:** `static formatCalendar(calendar : Calendar) : String` Formats a Calendar object with Calendar.INPUT_DATE_TIME_PATTERN format of the current request locale, for example "MM/dd/yyyy h:mm a" for the locale en_US. ### formatCalendar **Signature:** `static formatCalendar(calendar : Calendar, format : String) : String` Formats a Calendar object with the provided date format. ### formatCalendar **Signature:** `static formatCalendar(calendar : Calendar, locale : String, pattern : Number) : String` Formats a Calendar object with the date format defined by the provided locale and Calendar pattern. ### formatDate **Signature:** `static formatDate(date : Date) : String` Formats a date with the default date format of the current site. ### formatDate **Signature:** `static formatDate(date : Date, format : String) : String` Formats a date with the provided date format. ### formatDate **Signature:** `static formatDate(date : Date, format : String, locale : String) : String` Formats a date with the provided date format in specified locale. ### formatInteger **Signature:** `static formatInteger(number : Number) : String` Returns a formatted integer number using the default integer format of the current site. ### formatMoney **Signature:** `static formatMoney(money : Money) : String` Formats a Money Object with the default money format of the current request locale. ### formatNumber **Signature:** `static formatNumber(number : Number) : String` Returns a formatted number using the default number format of the current site. ### formatNumber **Signature:** `static formatNumber(number : Number, format : String) : String` Returns a formatted string using the specified number and format. ### formatNumber **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String` Returns a formatted number as a string using the specified number format in specified locale. ### formatNumber **Signature:** `static formatNumber(number : Number, format : String) : String` Returns a formatted string using the specified number and format. ### formatNumber **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String` Returns a formatted number as a string using the specified number format in specified locale. ### garble **Signature:** `static garble(str : String, replaceChar : String, suffixLength : Number) : String` Return a string in which specified number of characters in the suffix is not changed and the rest of the characters replaced with specified character. ### ltrim **Signature:** `static ltrim(str : String) : String` Returns the string with leading white space removed. ### pad **Signature:** `static pad(str : String, width : Number) : String` This method provides cell padding functionality to the template. ### rtrim **Signature:** `static rtrim(str : String) : String` Returns the string with trailing white space removed. ### stringToHtml **Signature:** `static stringToHtml(str : String) : String` Convert a given string to an HTML-safe string. ### stringToWml **Signature:** `static stringToWml(str : String) : String` Converts a given string to a WML-safe string. ### stringToXml **Signature:** `static stringToXml(str : String) : String` Converts a given string to a XML-safe string. ### trim **Signature:** `static trim(str : String) : String` Returns the string with leading and trailing white space removed. ### truncate **Signature:** `static truncate(str : String, maxLength : Number, mode : String, suffix : String) : String` Truncate the string to the specified length using specified truncate mode. ## Method Detail ## Method Details ### decodeBase64 **Signature:** `static decodeBase64(base64 : String) : String` **Description:** Interprets a Base64 encoded string as byte stream of an UTF-8 encoded string. The method throws an IllegalArgumentException in case the encoding failed because of a mismatch between the input string and the character encoding. **Parameters:** - `base64`: the Base64 encoded string - should not be empty or null. **Returns:** the decoded string. --- ### decodeBase64 **Signature:** `static decodeBase64(base64 : String, characterEncoding : String) : String` **Description:** Interprets a Base64 encoded string as the byte stream representation of a string. The given character encoding is used for decoding the byte stream into the character representation. The method throws an IllegalArgumentException in case the encoding failed because of a mismatch between the input String and the character encoding. **Parameters:** - `base64`: the Base64 encoded string - should not be empty or null. - `characterEncoding`: the character encoding to read the input string - should not be empty or null. **Returns:** the decoded string. --- ### decodeString **Signature:** `static decodeString(str : String, type : Number) : String` **Description:** Convert a given syntax-safe string to a string according to the selected character entity encoding type. **Parameters:** - `str`: String to be decoded - `type`: decode type **Returns:** decoded string --- ### encodeBase64 **Signature:** `static encodeBase64(str : String) : String` **Description:** Encodes the byte representation of the given string as Base64. The string is converted into the byte representation with UTF-8 encoding. The method throws an IllegalArgumentException in case the encoding failed because of a mismatch between the input string and the character encoding. **Parameters:** - `str`: the string to encode - should not be empty or null. **Returns:** the encoded string. --- ### encodeBase64 **Signature:** `static encodeBase64(str : String, characterEncoding : String) : String` **Description:** Encodes the byte representation of the given string as Base64. The string is converted into the byte representation using the given character encoding. The method throws an IllegalArgumentException in case the encoding failed because of a mismatch between the input string and the character encoding. **Parameters:** - `str`: the string to encode - should not be empty or null. - `characterEncoding`: the character encoding to read the input string - should not be empty or null. **Returns:** the encoded string. --- ### encodeString **Signature:** `static encodeString(str : String, type : Number) : String` **Description:** Convert a given string to a syntax-safe string according to the selected character entity encoding type. **Parameters:** - `str`: String to be encoded - `type`: encode type **Returns:** encoded string --- ### format **Signature:** `static format(format : String, args : Object...) : String` **Description:** Returns a formatted string using the specified format and arguments. The formatting string is a Java MessageFormat expression, e.g. format( "Message: {0}, {1}", "test", 10 ) would result in "Message: test, 10". If a Collection is passed as the only argument, the elements of this collection are used as arguments for the formatting. **Parameters:** - `format`: Java like formatting string. - `args`: optional list of arguments or a collection, which are included into the result string **Returns:** the formatted result string. --- ### formatCalendar **Signature:** `static formatCalendar(calendar : Calendar) : String` **Description:** Formats a Calendar object with Calendar.INPUT_DATE_TIME_PATTERN format of the current request locale, for example "MM/dd/yyyy h:mm a" for the locale en_US. The used time zone is the time zone of the calendar object. **Parameters:** - `calendar`: the calendar object. **Returns:** a string representation of the formatted calendar object. --- ### formatCalendar **Signature:** `static formatCalendar(calendar : Calendar, format : String) : String` **Description:** Formats a Calendar object with the provided date format. The format is a Java date format, like "yyy-MM-dd". The used time zone is the time zone of the calendar object. **Parameters:** - `calendar`: the calendar object to be printed - `format`: the format to use. **Returns:** a string representation of the formatted calendar object. --- ### formatCalendar **Signature:** `static formatCalendar(calendar : Calendar, locale : String, pattern : Number) : String` **Description:** Formats a Calendar object with the date format defined by the provided locale and Calendar pattern. The locale can be for instance the request.getLocale(). The used time zone is the time zone of the calendar object. **Parameters:** - `calendar`: the calendar object to be printed - `locale`: the locale, which defines the date format to be used - `pattern`: the pattern is one of a calendar pattern e.g. SHORT_DATE_PATTERN as defined in the regional settings for the locale **Returns:** a string representation of the formatted calendar object. --- ### formatDate **Signature:** `static formatDate(date : Date) : String` **Description:** Formats a date with the default date format of the current site. **Deprecated:** Use formatCalendar(Calendar, String) instead. **Parameters:** - `date`: the date to format. **Returns:** a string representation of the formatted date. --- ### formatDate **Signature:** `static formatDate(date : Date, format : String) : String` **Description:** Formats a date with the provided date format. The format is the Java date format, like "yyyy-MM-DD". The locale of the calling context request is used in formatting. **Deprecated:** Use formatCalendar(Calendar, String) instead. **Parameters:** - `date`: the date to format. - `format`: the format to use. **Returns:** a string representation of the formatted date. --- ### formatDate **Signature:** `static formatDate(date : Date, format : String, locale : String) : String` **Description:** Formats a date with the provided date format in specified locale. The format is Java date format, like "yyyy-MM-DD". **Deprecated:** Use formatCalendar(Calendar, String) instead. **Parameters:** - `date`: the date to format. - `format`: the format to use. - `locale`: the locale to use. **Returns:** a string representation of the formatted date. --- ### formatInteger **Signature:** `static formatInteger(number : Number) : String` **Description:** Returns a formatted integer number using the default integer format of the current site. The method can be also called to format a floating number as integer. **Parameters:** - `number`: the number to format. **Returns:** a formatted an integer number with the default integer format of the current site. --- ### formatMoney **Signature:** `static formatMoney(money : Money) : String` **Description:** Formats a Money Object with the default money format of the current request locale. **Parameters:** - `money`: The Money instance that should be formatted. **Returns:** The formatted String representation of the passed money. In case of an error the string 'N/A' is returned. --- ### formatNumber **Signature:** `static formatNumber(number : Number) : String` **Description:** Returns a formatted number using the default number format of the current site. Decimal and grouping separators are used as specified in the locales regional settings. **Parameters:** - `number`: the number to format. **Returns:** a formatted number using the default number format of the current site. --- ### formatNumber **Signature:** `static formatNumber(number : Number, format : String) : String` **Description:** Returns a formatted string using the specified number and format. The format is Java number format, like "#,###.00". To format as an integer number provide "0" as format string. The locale of the calling context request is used in formatting. **API Versioned:** No longer available as of version 18.10. **Parameters:** - `number`: the number to format. - `format`: the format to use. **Returns:** a formatted string using the specified number and format. --- ### formatNumber **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String` **Description:** Returns a formatted number as a string using the specified number format in specified locale. The format is Java number format, like "#,###.00". To format as an integer number provide "0" as format string. **API Versioned:** No longer available as of version 18.10. **Parameters:** - `number`: the number to format. - `format`: the format to use. - `locale`: the locale to use. **Returns:** a formatted number as a string using the specified number format in specified locale. --- ### formatNumber **Signature:** `static formatNumber(number : Number, format : String) : String` **Description:** Returns a formatted string using the specified number and format. The format is Java number format, like "#,###.00". To format as an integer number provide "0" as format string. The locale of the calling context request is used in formatting. Decimal and grouping separators are used as specified in the locales regional settings (when configured, otherwise a fallback to the internal configuration is done). **API Versioned:** From version 18.10. In prior versions this method did fall back to Java formatting rules, instead of using the definitions in regional settings. **Parameters:** - `number`: the number to format. - `format`: the format to use. **Returns:** a formatted string using the specified number and format. --- ### formatNumber **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String` **Description:** Returns a formatted number as a string using the specified number format in specified locale. The format is Java number format, like "#,###.00". To format as an integer number provide "0" as format string. Decimal and grouping separators are used as specified in the locales regional settings (when configured, otherwise a fallback to the internal configuration is done). **API Versioned:** From version 18.10. In prior versions this method did fall back to Java formatting rules, instead of using the definitions in regional settings. **Parameters:** - `number`: the number to format. - `format`: the format to use. - `locale`: the locale to use. **Returns:** a formatted number as a string using the specified number format in specified locale. --- ### garble **Signature:** `static garble(str : String, replaceChar : String, suffixLength : Number) : String` **Description:** Return a string in which specified number of characters in the suffix is not changed and the rest of the characters replaced with specified character. **Parameters:** - `str`: String to garble - `replaceChar`: character to use as a replacement - `suffixLength`: length of the suffix **Returns:** the garbled string. --- ### ltrim **Signature:** `static ltrim(str : String) : String` **Description:** Returns the string with leading white space removed. **Parameters:** - `str`: the String to remove characters from. **Returns:** the string with leading white space removed. --- ### pad **Signature:** `static pad(str : String, width : Number) : String` **Description:** This method provides cell padding functionality to the template. **Parameters:** - `str`: the string to process - `width`: The absolute value of this number defines the width of the cell. A possitive number forces left, a negative number right alignment. A '0' doesn't change the string. **Returns:** the processed string. --- ### rtrim **Signature:** `static rtrim(str : String) : String` **Description:** Returns the string with trailing white space removed. **Parameters:** - `str`: the String to remove characters from. **Returns:** the string with trailing white space removed. --- ### stringToHtml **Signature:** `static stringToHtml(str : String) : String` **Description:** Convert a given string to an HTML-safe string. This method substitutes characters that conflict with HTML syntax (<,>,&,") and characters that are beyond the ASCII chart (Unicode 160-255) to HTML 3.2 named character entities. **Parameters:** - `str`: String to be converted. **Returns:** converted string. --- ### stringToWml **Signature:** `static stringToWml(str : String) : String` **Description:** Converts a given string to a WML-safe string. This method substitutes characters that conflict with WML syntax (<,>,&,',"$) to WML named character entities. **Deprecated:** Don't use this method anymore **Parameters:** - `str`: String to be converted. **Returns:** the converted string. --- ### stringToXml **Signature:** `static stringToXml(str : String) : String` **Description:** Converts a given string to a XML-safe string. This method substitutes characters that conflict with XML syntax (<,>,&,',") to XML named character entities. **Parameters:** - `str`: String to be converted. **Returns:** the converted string. --- ### trim **Signature:** `static trim(str : String) : String` **Description:** Returns the string with leading and trailing white space removed. **Parameters:** - `str`: the string to trim. **Returns:** the string with leading and trailing white space removed. --- ### truncate **Signature:** `static truncate(str : String, maxLength : Number, mode : String, suffix : String) : String` **Description:** Truncate the string to the specified length using specified truncate mode. Optionally, append suffix to truncated string. **Parameters:** - `str`: string to truncate - `maxLength`: maximum length of the truncated string, not including suffix - `mode`: truncate mode (TRUNCATE_CHAR, TRUNCATE_WORD, TRUNCATE_SENTENCE), if null TRUNCATE_CHAR is assumed - `suffix`: suffix append to the truncated string **Returns:** the truncated string. --- ``` -------------------------------------------------------------------------------- /docs/best-practices/performance.md: -------------------------------------------------------------------------------- ```markdown # Salesforce B2C Commerce Cloud: Performance Best Practices This document outlines key performance optimization strategies for Salesforce B2C Commerce Cloud, focusing on caching and efficient data retrieval. --- ## Performance and Stability Coding Standards Ecommerce applications built on Salesforce B2C Commerce can run fast and perform reliably. Use B2C Commerce within its capabilities and ensure that your customizations follow coding best practices. Identify permissible designs that ensure the scalability and robustness of your customizations. ### Data Transfer Volume B2C Commerce imposes limits on incoming and outgoing network traffic for B2C Commerce instances and the Content Delivery Network. These limits are relative to the Gross Merchandise Value (GMV) and are defined in the Main Subscription Agreement (MSA). Data transfers within the limits are at no additional charge (are included in the subscription fee). There is a fee for data transfers exceeding the limits. ### Storefront Development for Performance and Stability When developing your storefront, consider storefront development best practices: #### Search and Product Processing - **Don't post-process product or content search results.** Search results can be large sets. Instead, all search criteria must go into the query for efficiency execution. Don't post-process with custom code. - **Don't iterate over variations of a base product on a search result page** (or any page where multiple base products appear). This approach can significantly increase the number of touched business objects. Instead, use native Salesforce B2C Commerce features: - Use pipelet Search with input parameter `OrderableProductsOnly` to deal with variation product availability - Use `dw.catalog.ProductSearchHit.getRepresentedVariationValues()` to determine available variation values - Use `dw.catalog.ProductSearchHit.minPrice` or `Product.priceMode.minPrice` to determine price ranges - **Break search results into pages** before processing or displaying in the storefront (pipelet Paging, class PagingModel). Limit the maximum page size, for example, a maximum of 120 products per page, especially if the "View All" functionality is provided. #### External System Integration - **Don't trigger live calls to external systems on frequently visited pages** (homepage, category, search result pages, and product pages). Where live calls are needed, specify a low timeout value (for example, 1 second). A B2C Commerce application server thread waiting for a response from an external system can't serve other requests. Many threads waiting for responses can make the entire cluster unresponsive. #### Long-Running Operations - **Don't execute any long running operations in a storefront controller or pipeline** (for example, import or export). Instead, use "jobs" for all long running tasks. The web tier closes browser connections after 5 minutes. The controller or pipeline could still be running at this time. #### Concurrency and Data Integrity - **Avoid concurrent changes to the same object.** Storefront controllers and pipelines should only: - Read shared data (for example, catalogs and prices) - Read or write customer-specific data (for example, customer profiles, shopping carts or orders) - The inventory framework is designed to support concurrent change (for example, two customers buying the same product at the same time or a customer buying a product while the inventory import is running). - The storefront controller or pipeline marks the order with `EXPORT_STATUS_READY` as the last step in order creation. Then order processing jobs can start modifying the order object. - Concurrent requests for the same session are serialized at the application server. Concurrent Script API controller or pipeline requests can lead to Optimistic Locking exceptions. #### Transaction Management - **Limit transaction size.** The system is designed to deal with transactions with up to 1,000 modified business objects. A storefront controller or pipeline shouldn't even come close to this number. #### Critical Page Performance - **Make sure that the most visited pages are cacheable and well performing.** These controllers and pages are usually: - Category page or search result pages (Search-Show) - Product detail pages (Product-Show) - Home pages (Default-Start, Home-Show) - Cart Page (Cart-Show) - Checkout pages - **Limit expensive (> 10 ms) custom server logic** on OnSession and OnRequest controllers. ### Use Index-Friendly APIs Replace database intensive or inefficient APIs with appropriate index-friendly APIs. Check code for database intensive APIs in most-visited pages: #### Avoid These Database-Intensive APIs: - `Category.getProducts()` - `Category.getOnlineProducts()` - `Category.getProductAssignments()` - `Category.getOnlineCategoryAssignments()` - `ProductMgr.queryAllSiteProducts()` - `Product.getPriceModel()` - `Product.getVariants()` - `Product.getVariationModel()` #### Use These Index-Friendly APIs Instead: - `ProductSearchModel.search()` - `ProductSearchModel.orderableProductsOnly(true)` - `ProductSearchModel.getRefinements()` - `ProductSearchRefinements.getNextLevelRefinementValues()` - `ProductSearchModel.getProductSearchHits()` - `ProductSearchHit.getMinPrice()` - `ProductSearchHit.getMaxPrice()` - `ProductSearchHit.getRepresentedProductIDs()` - `ProductSearchHit.getRepresentedVariationValues(attribute)` ### Additional Performance Requirements - **Ensure all direct 3rd-party HTTP calls are migrated to Web Service Framework** - **Ensure no Enforced quota violations** are reported in STAGING and PRODUCTION, and that Quota Dashboard alerts have been subscribed by all site admins - **Ensure there isn't unnecessary creation** of custom Session objects, productlist objects, or cookies - **Ensure a WishList isn't created for every anonymous user** (e.g., created at the end of every product item add to cart calls) - **Ensure Custom Object volume is kept in check** with purge jobs #### OCAPI Specific Requirements: - **Ensure Shop API GET requests are limited to smaller blocks of data.** Instead of 200 products payload, retrieve 100 or 50 - **Ensure there's no OCAPI request of persistent objects within a hook customization** such as `ProductMgr.getProduct()` or `product.getVariations()` #### SFRA Specific Requirements: - **Ensure SFRA templates don't include multi-part, embedded, or nested forms.** We don't recommend them as a best practice - **Ensure that controllers don't call each other,** because controller functionality should be self-contained to avoid circular dependencies - **Ensure no calling pipelets from within a controller.** It's allowed while there are still pipelets that don't have equivalent B2C Commerce script methods, but won't be supported in future ### Job Development for Performance and Stability To optimize job performance, follow the job development standards: #### Import and Data Processing - **To modify objects in Salesforce B2C Commerce, use standard imports instead of customizations.** In jobs, use B2C Commerce Job Steps for imports. - **B2C Commerce standard imports are designed to process arbitrary feed sizes.** Changes are committed to the database on a per business object basis. If related changes must be committed in a single transaction, enclose the import pipelets in an explicit controller or pipeline transaction. Choose this approach only as an exception. - **The transaction size is limited to 1,000 modified business objects.** Ensure that this limit isn't exceeded. B2C Commerce does not enforce this limit today, but might in the future. #### Data Quality and Validation - **Don't implement data validation jobs on B2C Commerce** (for example, products with no names or $0 prices). Instead, ensure that the feeds into B2C Commerce are of high quality, and don't include products with incomplete attribution or are marked offline. You can manually review catalog data on a staging instance. #### Memory Management When processing large data sets, pay attention to the memory footprint: - **Design loop logic so that memory consumption doesn't increase with result set size** - **Keep only currently processed objects in memory,** and do not retain references to that object (so that the object can be freed from memory). Specifically, don't perform sorting or other types of collections in memory - **Stream data to file regularly** (do not build large structures in memory) - **Read feeds record by record** (do not read an entire file into memory) - **If you must create multiple feeds,** query the objects once and write records to all feeds as you iterate over the results. This approach saves time because the objects must be created in memory only once #### Concurrency and Resource Management - **Avoid concurrent changes to the same object.** Use the locking framework to ensure exclusive access. Specify named resources for job schedules. - **Keep application server utilization by jobs to a minimum.** Calculate the job load factor: total number of seconds of job execution time on an instance (Staging or Production) on a day divided by 86,400 (number of seconds in a day). Try to keep the job load factor below 0.20. #### Recovery and Reliability - **Pay attention to recovery in solution design.** A job might end abnormally, for example, server restart or application server failure. The job can be resumed or restarted. Design the job so that it recovers gracefully. It must be possible to repeat a job step that was aborted. - **Don't start many jobs at the same time.** Instead, disperse job start times to balance the job load. --- ## 1. Page Caching The web-server page cache is the most critical performance feature for server-rendered storefronts. The goal is to serve fully rendered HTML from this cache to avoid hitting the application server. ### Controller-Driven Caching (Best Practice) Control caching within your controller using the response object. This is superior to the legacy `<iscache>` tag. `response.setExpires(milliseconds)`: Sets a cache duration for the entire page response. **Example:** ```javascript // cartridge/controllers/Product.js var server = require('server'); server.get('Show', function (req, res, next) { // Cache for 24 hours var oneDay = 24 * 60 * 60 * 1000; response.setExpires(Date.now() + oneDay); res.render('product/productDetails'); next(); }); ``` ### Remote Includes for Dynamic Content Use remote includes (`<isinclude url="..." />`) to assemble pages from components with different cache policies. A long-cached main page can include a dynamic, non-cached header with user-specific info. [1, 2] **Anti-Pattern:** Avoid creating remote includes with unique URL parameters for each item in a list (e.g., `&position=1`, `&position=2`). This creates an N+1 request problem at the HTTP level and defeats the cache. [1, 3] ### Cache Key Strategy The cache key is the full URL. To maximize the cache hit ratio: - **Ignore Volatile Parameters:** Use Business Manager (`Administration > Sites > Feature Switches`) to ignore marketing parameters (e.g., `utm_source`, `utm_campaign`) when generating the cache key. [4, 5] - **Personalized Caching:** Use `response.setVaryBy('price_promotion')` to create separate cache entries for users with different prices or promotions. Use this carefully, as it can fragment the cache. [4, 5] --- ## 2. Custom Caches (`CacheMgr`) Use custom caches for application-level data caching within dynamic requests (e.g., cart, checkout) where page caching isn't possible. **Use Cases:** - Caching expensive calculations (e.g., iterating variants to check for a sale). - Caching responses from external services (e.g., inventory, ratings). ### Implementation 1. **Define in `caches.json`:** ```json { "caches": [ { "name": "ExternalAPICache", "ttl": 300 } ] } ``` 2. **Register in `package.json`:** ```json { "caches": "./cartridge/scripts/caches.json" } ``` ### The "Get-or-Load" Pattern (Required) Always use the atomic `cache.get(key, loader)` method to prevent a "thundering herd" problem on cache misses. [6, 9] **Example:** ```javascript var CacheMgr = require('dw/system/CacheMgr'); var MyHTTPService = require('~/cartridge/scripts/services/myHTTPService'); function getExternalData() { var apiCache = CacheMgr.getCache('ExternalAPICache'); var cacheKey = 'myExternalData'; // get() executes the loader function ONLY on a cache miss. var data = apiCache.get(cacheKey, function () { // This expensive call only runs if data is not in cache. var result = MyHTTPService.getService().call(); return result.ok ? JSON.parse(result.object.text) : null; }); return data; } ``` **Key Limitation:** Custom caches are local to each application server pod and are not a distributed, instance-wide cache. Data stored on one pod is not visible to others. ## 3. ProductSearchModel vs. ProductMgr This is a critical performance distinction. - **ProductSearchModel (PSM):** Queries the fast, optimized Search Index. Use for any list of products (PLPs, search results, filtering). - **ProductMgr:** Queries the live Database. Use only to get a single, known product by its ID (e.g., on a PDP). ### The N+1 Anti-Pattern (CRITICAL) **NEVER** use `ProductMgr.getProduct()` inside a loop over ProductSearchModel results. This causes one fast index query followed by N slow database queries, which will crash a site under load. **Incorrect (Anti-Pattern):** ```javascript // In a PLP template, looping over search results var psm = new ProductSearchModel(); //... configure psm... psm.search(); var hits = psm.getProductSearchHits(); while (hits.hasNext()) { var hit = hits.next(); // ANTI-PATTERN: Calling ProductMgr in a loop! var product = ProductMgr.getProduct(hit.getProductID()); //... do something with the full product object... } ``` **Correct:** Use the ProductSearchHit object directly. It contains all necessary data from the index for display on a listing page. If data is missing, add it to the search index configuration. ```javascript // In a PLP template, looping over search results var psm = new ProductSearchModel(); //... configure psm... psm.search(); var hits = psm.getProductSearchHits(); while (hits.hasNext()) { var hit = hits.next(); // CORRECT: Use the hit object directly for name, price, etc. var minPrice = hit.getMinPrice(); var variationValues = hit.getRepresentedVariationValues('color'); //... render tile using data from 'hit'... } ``` ## 4. Caching in OCAPI/SCAPI Hooks The caching models for OCAPI and SCAPI are fundamentally different. - **OCAPI:** The hook runs before the response is cached. The modified response is what gets stored in OCAPI's application-tier cache. - **SCAPI:** The web-tier cache is checked before the hook runs. The hook only executes on a cache miss. The original, unmodified response from the platform is what gets cached. ### SCAPI Web-Tier Cache Fundamentals One of the Commerce Cloud application layer components performs web-tier caching for SCAPI **GET** requests across multiple API families. This cache lives on the server side and is applied only after a request reaches the platform. Any additional caching layers you add (CDN, browser, SPA state) operate independently—you could have a cache miss on the web tier but a hit in your edge cache, and vice versa. Plan your caching strategy with this multi-layer reality in mind. ### Personalized Cache Keys When personalization is enabled for a SCAPI resource, the cache key includes the following in addition to the URL string: - Active promotions - Active product sorting rules - Applicable price books - Active AB test groups The platform keeps separate cache entries for each combination. For example, if shopper A qualifies for promotion X and shopper B qualifies for promotion Y, the same product URL produces two cache entries. This segmentation can be powerful but also multiplies cache storage. Use personalization only when you have well-sized groups and a clear business reason. By default, product requests that expand prices or promotions—and product search requests with the `prices` expand—are already personalized. Calling `response.setVaryBy('price_promotion')` in a script reinforces that behavior. Note that `price_promotion` is the only supported value; other strings have no effect. ### Script-Level Cache Controls Use the Script API to adjust cache policies dynamically: - `dw.system.Response#setExpires(milliseconds)`: Sets an explicit expiration timestamp. The value must be at least 1,000 ms in the future and no more than 86,400,000 ms (24 hours). - `dw.system.Response#setVaryBy('price_promotion')`: Opts into personalized caching for price- or promotion-sensitive responses. ```javascript exports.modifyGETResponse = function (scriptCategory, categoryWO) { // Cache for one hour instead of the default TTL response.setExpires(Date.now() + 3_600_000); // Optional: personalize by price & promotion eligibility response.setVaryBy('price_promotion'); return new Status(Status.OK); }; ``` ### Best Practices - **OCAPI Hook:** Your modifications will be cached. Keep the logic simple and avoid slow calls like `ProductMgr.getProduct()`. - **SCAPI Hook:** To cache a modification, you must create a unique cache key by adding a custom query parameter to the URL. **Example (SCAPI):** ```javascript // Client makes a call with a custom parameter // GET /shopper-products/v1/.../products/my-prod?c_view=light // SCAPI Hook Script (product.js) exports.modifyResponse = function (product, productResponse) { // Logic is conditional on the custom parameter if (request.httpParameters.c_view === 'light') { delete productResponse.long_description; } }; ``` This creates a separate, cacheable version of the response for the `c_view=light` URL. **SCAPI expand Parameter:** The cache TTL for a SCAPI response is determined by the lowest TTL of all requested expand parameters. Avoid requesting volatile data (like availability, 60s TTL) alongside stable data (like images, 24hr TTL). ## 5. Caching in Custom SCAPI Endpoints You can build your own REST endpoints that integrate with SCAPI's caching, security, and other framework features. ### Implementation Custom endpoints can leverage the same powerful web-tier page cache. Enable it by calling `response.setExpires()` in your implementation script. **Example:** ```javascript // cartridge/rest-apis/my-api/v1/script.js var RESTResponseMgr = require('dw/system/RESTResponseMgr'); exports.getLoyaltyInfo = function (params) { var loyaltyData = { id: params.c_customer_id, points: 1234 }; // Cache this custom API response for 5 minutes response.setExpires(Date.now() + (5 * 60 * 1000)); RESTResponseMgr.createSuccess(loyaltyData).render(); }; exports.getLoyaltyInfo.public = true; ``` ### Two-Tier Caching Pattern (for External Services) For maximum resilience and performance when calling external systems, combine both cache layers: 1. **Tier 1 (Application Cache):** Use CacheMgr with the "get-or-load" pattern to cache the raw data from the external service. This acts as a buffer if the service is slow or down. 2. **Tier 2 (Web-Tier Cache):** In your custom endpoint script, after getting data from the Tier 1 cache, format the final JSON response and set a web-tier cache policy on it using `response.setExpires()`. This pattern ensures that most requests are served instantly from the web-tier, and even on a miss, the data is likely served from the fast application-tier cache, minimizing slow calls to the external dependency. ``` -------------------------------------------------------------------------------- /docs/dw_order/BasketMgr.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.order # Class BasketMgr ## Inheritance Hierarchy - Object - dw.order.BasketMgr ## Description Provides static helper methods for managing baskets. ## Properties ### baskets **Type:** List (Read Only) Retrieve all open baskets for the logged in customer including the temporary baskets. Restricted to agent scenario use cases: The returned list contains all agent baskets created with createAgentBasket() and the current storefront basket which can also be retrieved with getCurrentBasket(). This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore. ### currentBasket **Type:** Basket (Read Only) This method returns the current valid basket of the session customer or null if no current valid basket exists. The methods getCurrentBasket() and getCurrentOrNewBasket() work based on the selected basket persistence, which can be configured in the Business Manager site preferences / baskets section. A basket is valid for the configured basket lifetime. In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also utilizing PWA Kit or other custom headless solution for another part of the same site), this method must NOT be used. Instead, retrieve baskets via GET baskets/{basketId} or GET customers/{customerId}/baskets. Do not use getCurrentOrNewBasket() for basket creation in any scenario. The current basket, if one exists, is usually updated by the method. In particular the last-modified date is updated. No update is done when method getCurrentBasket() is used within a read-only hook implementation (such as a beforeGet or a modifyResponse hook). The lifetime of a basket can be extended in 2 ways: The basket is modified in some way, e.g. a product is added resulting in the basket total being newly calculated. This results in the basket lifetime being reset. The basket has not been modified for 60 minutes, then using this method to access the basket will also reset the basket lifetime. What happens when a customer logs in? Personal data held inside the basket such as addresses, email addresses and payment settings is associated with the customer to whom the basket belongs. If the basket being updated belongs to a different customer this data is removed. This happens when a guest customer that has a basket logs in and hence identifies as a registered customer. In this case the basket which was previously created by the guest customer gets transferred to the (now logged in) registered customer. Should the registered customer already have a basket, this basket is effectively invalidated, but made available using getStoredBasket() allowing the script to merge content from it if desired. What happens when a customer logs out or when the customer session times out? After the customer logs out, a basket belonging to the registered customer (now logged out) is stored (where applicable) and this method returns null. Personal data is also cleared when the session times out for a guest customer. The following personal data is cleared: product line items that were added from a wish list shipping method coupon line items gift certificate line items billing and shipping addresses payment instruments buyer email If the session currency no longer matches the basket currency, the basket currency should be updated with Basket.updateCurrency(). Typical usage: var basket : Basket = BasketMgr.getCurrentBasket(); if (basket) { // do something with basket } Constraints: The method only accesses the basket for the session customer, an exception is thrown when the session customer is null. Method getCurrentOrNewBasket() only creates a basket when method getCurrentBasket() returns null. ### currentOrNewBasket **Type:** Basket (Read Only) This method returns the current valid basket of the session customer or creates a new one if no current valid basket exists. See getCurrentBasket() for more details. In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also utilizing PWA Kit or other custom headless solution for another part of the same site), this method must NOT be used. For these scenarios, create baskets via POST baskets REST calls. ### storedBasket **Type:** Basket (Read Only) This method returns the stored basket of the session customer or null if none is found. A stored basket is returned in the following situation: During one visit, a customer-Q logs in and creates a basket-A by adding products to it. In a later visit, a second basket-B is created for a guest customer who then logs in as customer-Q. In this case basket-B is reassigned to customer-Q and basket-A is accessible as the stored basket using this method. Now it is possible to merge the information from the stored basket to the active basket. A stored basket will exist only if the corresponding setting is selected in the Business Manager site preferences' baskets section. A basket is valid for the configured basket lifetime. Typical usage: var currentBasket : Basket = BasketMgr.getCurrentOrNewBasket(); var storedBasket : Basket = BasketMgr.getStoredBasket(); if (storedBasket) { // transfer all the data needed from the stored to the active basket } ### temporaryBaskets **Type:** List (Read Only) Retrieve all open temporary baskets for the logged in customer. Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore. ## Constructor Summary ## Method Summary ### createAgentBasket **Signature:** `static createAgentBasket() : Basket` Creates a new agent basket for the current session customer. ### createBasketFromOrder **Signature:** `static createBasketFromOrder(order : Order) : Basket` Creates a Basket from an existing Order for the purposes of changing an Order. ### createTemporaryBasket **Signature:** `static createTemporaryBasket() : Basket` Creates a new temporary basket for the current session customer. ### deleteBasket **Signature:** `static deleteBasket(basket : Basket) : void` Remove a customer basket including a temporary basket. ### deleteTemporaryBasket **Signature:** `static deleteTemporaryBasket(basket : Basket) : void` Remove a customer temporary basket. ### getBasket **Signature:** `static getBasket(uuid : String) : Basket` This method returns a valid basket of the session customer or null if none is found. ### getBaskets **Signature:** `static getBaskets() : List` Retrieve all open baskets for the logged in customer including the temporary baskets. ### getCurrentBasket **Signature:** `static getCurrentBasket() : Basket` This method returns the current valid basket of the session customer or null if no current valid basket exists. ### getCurrentOrNewBasket **Signature:** `static getCurrentOrNewBasket() : Basket` This method returns the current valid basket of the session customer or creates a new one if no current valid basket exists. ### getStoredBasket **Signature:** `static getStoredBasket() : Basket` This method returns the stored basket of the session customer or null if none is found. ### getTemporaryBasket **Signature:** `static getTemporaryBasket(uuid : String) : Basket` This method returns a valid temporary basket of the session customer or null if none is found. ### getTemporaryBaskets **Signature:** `static getTemporaryBaskets() : List` Retrieve all open temporary baskets for the logged in customer. ## Method Detail ## Method Details ### createAgentBasket **Signature:** `static createAgentBasket() : Basket` **Description:** Creates a new agent basket for the current session customer. By default only 4 open agent baskets are allowed per customer. If this is exceeded a CreateAgentBasketLimitExceededException will be thrown. This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. **Returns:** the newly created basket for the customer which is logged in **Throws:** CreateAgentBasketLimitExceededException - indicates that no agent basket could be created because the agent basket limit is already exceeded --- ### createBasketFromOrder **Signature:** `static createBasketFromOrder(order : Order) : Basket` **Description:** Creates a Basket from an existing Order for the purposes of changing an Order. When an Order is later created from the Basket, the original Order is changed to status Order.ORDER_STATUS_REPLACED. Restricted to agent scenario use cases. In case a storefront customer is using it the created storefront basket cannot be retrieved via getCurrentBasket() (ScriptAPI), GET /baskets/<basketid> (REST APIs) or DELETE /baskets/<basketid> (REST APIs) or GetBasket (Pipelet) or Basket-related CSC Operations from BM (these also use OCAPI REST API). Baskets containing an "orderNumberBeingEdited" are explicitly excluded from the list of baskets that can be retrieved. Responsible for this behavior (this kind of basket cannot be used as general purpose shopping baskets) - see Basket.getOrderNoBeingEdited() / Basket.getOrderBeingEdited(). In case a Business Manager user is logged in into the session the basket will be marked as an agent basket. See Basket.isAgentBasket(). Any inventory reservation associated with the order will be canceled either early when Basket.reserveInventory() is called for the new basket or (later) when a new replacement order is created from the basket. Consider reserving the basket following its creation. The method only succeeds for an Order without gift certificates, status is not cancelled, was not previously replaced and was not previously exported. Failures are indicated by throwing an APIException of type CreateBasketFromOrderException which provides one of these errorCodes: Code OrderProcessStatusCodes.ORDER_CONTAINS_GC - the Order contains a gift certificate and cannot be replaced. Code OrderProcessStatusCodes.ORDER_ALREADY_REPLACED - the Order was already replaced. Code OrderProcessStatusCodes.ORDER_ALREADY_CANCELLED - the Order was cancelled. Code OrderProcessStatusCodes.ORDER_ALREADY_EXPORTED - the Order has already been exported. Usage: var order : Order; // known try { var basket : Basket = BasketMgr.createBasketFromOrder(order); } catch (e) { if (e instanceof APIException && e.type === 'CreateBasketFromOrderException') { // handle e.errorCode } } **Parameters:** - `order`: Order to create a Basket for **Returns:** a new Basket **See Also:** AgentUserMgr.loginAgentUser(String, String) AgentUserMgr.loginOnBehalfOfCustomer(Customer) **Throws:** - `CreateBasketFromOrderException`: indicates the Order is in an invalid state. --- ### createTemporaryBasket **Signature:** `static createTemporaryBasket() : Basket` **Description:** Creates a new temporary basket for the current session customer. Temporary baskets are separate from shopper storefront and agent baskets, and are intended for use to perform calculations or create an order without disturbing a shopper's open storefront basket. Temporary baskets are automatically deleted after a time duration of 15 minutes. By default only 4 open temporary baskets are allowed per customer. If this is exceeded a CreateTemporaryBasketLimitExceededException will be thrown. **Returns:** the newly created basket for the current session customer --- ### deleteBasket **Signature:** `static deleteBasket(basket : Basket) : void` **Description:** Remove a customer basket including a temporary basket. This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. **Parameters:** - `basket`: the basket to be removed **See Also:** AgentUserMgr.loginAgentUser(String, String) AgentUserMgr.loginOnBehalfOfCustomer(Customer) --- ### deleteTemporaryBasket **Signature:** `static deleteTemporaryBasket(basket : Basket) : void` **Description:** Remove a customer temporary basket. **Parameters:** - `basket`: the temporary basket to be removed --- ### getBasket **Signature:** `static getBasket(uuid : String) : Basket` **Description:** This method returns a valid basket of the session customer or null if none is found. This method can also be used to get a temporary basket for the session customer. If the basket does not belong to the session customer, the method returns null. If the registered customer is not logged in, the method returns null. Restricted to agent scenario use cases: This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. The basket, if accessible, is usually updated in the same way as getCurrentBasket(). If the session currency no longer matches the basket currency, the basket currency should be updated with Basket.updateCurrency(). **Parameters:** - `uuid`: the id of the requested basket. **Returns:** the basket or null --- ### getBaskets **Signature:** `static getBaskets() : List` **Description:** Retrieve all open baskets for the logged in customer including the temporary baskets. Restricted to agent scenario use cases: The returned list contains all agent baskets created with createAgentBasket() and the current storefront basket which can also be retrieved with getCurrentBasket(). This method will result in an exception if called by a user without permission Create_Order_On_Behalf_Of or if no customer is logged in the session. Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore. **Returns:** all open baskets --- ### getCurrentBasket **Signature:** `static getCurrentBasket() : Basket` **Description:** This method returns the current valid basket of the session customer or null if no current valid basket exists. The methods getCurrentBasket() and getCurrentOrNewBasket() work based on the selected basket persistence, which can be configured in the Business Manager site preferences / baskets section. A basket is valid for the configured basket lifetime. In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also utilizing PWA Kit or other custom headless solution for another part of the same site), this method must NOT be used. Instead, retrieve baskets via GET baskets/{basketId} or GET customers/{customerId}/baskets. Do not use getCurrentOrNewBasket() for basket creation in any scenario. The current basket, if one exists, is usually updated by the method. In particular the last-modified date is updated. No update is done when method getCurrentBasket() is used within a read-only hook implementation (such as a beforeGet or a modifyResponse hook). The lifetime of a basket can be extended in 2 ways: The basket is modified in some way, e.g. a product is added resulting in the basket total being newly calculated. This results in the basket lifetime being reset. The basket has not been modified for 60 minutes, then using this method to access the basket will also reset the basket lifetime. What happens when a customer logs in? Personal data held inside the basket such as addresses, email addresses and payment settings is associated with the customer to whom the basket belongs. If the basket being updated belongs to a different customer this data is removed. This happens when a guest customer that has a basket logs in and hence identifies as a registered customer. In this case the basket which was previously created by the guest customer gets transferred to the (now logged in) registered customer. Should the registered customer already have a basket, this basket is effectively invalidated, but made available using getStoredBasket() allowing the script to merge content from it if desired. What happens when a customer logs out or when the customer session times out? After the customer logs out, a basket belonging to the registered customer (now logged out) is stored (where applicable) and this method returns null. Personal data is also cleared when the session times out for a guest customer. The following personal data is cleared: product line items that were added from a wish list shipping method coupon line items gift certificate line items billing and shipping addresses payment instruments buyer email If the session currency no longer matches the basket currency, the basket currency should be updated with Basket.updateCurrency(). Typical usage: var basket : Basket = BasketMgr.getCurrentBasket(); if (basket) { // do something with basket } Constraints: The method only accesses the basket for the session customer, an exception is thrown when the session customer is null. Method getCurrentOrNewBasket() only creates a basket when method getCurrentBasket() returns null. **Returns:** the current basket or null if no valid current basket exists. --- ### getCurrentOrNewBasket **Signature:** `static getCurrentOrNewBasket() : Basket` **Description:** This method returns the current valid basket of the session customer or creates a new one if no current valid basket exists. See getCurrentBasket() for more details. In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also utilizing PWA Kit or other custom headless solution for another part of the same site), this method must NOT be used. For these scenarios, create baskets via POST baskets REST calls. **Returns:** the basket, existing or newly created --- ### getStoredBasket **Signature:** `static getStoredBasket() : Basket` **Description:** This method returns the stored basket of the session customer or null if none is found. A stored basket is returned in the following situation: During one visit, a customer-Q logs in and creates a basket-A by adding products to it. In a later visit, a second basket-B is created for a guest customer who then logs in as customer-Q. In this case basket-B is reassigned to customer-Q and basket-A is accessible as the stored basket using this method. Now it is possible to merge the information from the stored basket to the active basket. A stored basket will exist only if the corresponding setting is selected in the Business Manager site preferences' baskets section. A basket is valid for the configured basket lifetime. Typical usage: var currentBasket : Basket = BasketMgr.getCurrentOrNewBasket(); var storedBasket : Basket = BasketMgr.getStoredBasket(); if (storedBasket) { // transfer all the data needed from the stored to the active basket } **Returns:** the stored basket or null if no valid stored basket exists. --- ### getTemporaryBasket **Signature:** `static getTemporaryBasket(uuid : String) : Basket` **Description:** This method returns a valid temporary basket of the session customer or null if none is found. If the basket does not belong to the session customer, the method returns null. If the basket is not a temporary basket, the method returns null. The basket, if accessible, is usually updated in the same way as getCurrentBasket(). If the session currency no longer matches the basket currency, the basket currency should be updated with Basket.updateCurrency(). **Parameters:** - `uuid`: the id of the requested temporary basket. **Returns:** the temporary basket or null --- ### getTemporaryBaskets **Signature:** `static getTemporaryBaskets() : List` **Description:** Retrieve all open temporary baskets for the logged in customer. Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore. **Returns:** all open temporary baskets --- ``` -------------------------------------------------------------------------------- /tests/mcp/node/search-system-object-attribute-groups.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript /** * Node.js programmatic tests for search_system_object_attribute_groups MCP tool (Full Mode) * * These tests provide comprehensive validation of the tool's functionality in full mode * with real SFCC OCAPI integration, including complex query scenarios, error handling, * performance validation, and integration testing. * * Test Categories: * 1. Basic Functionality Tests * 2. Complex Query Scenarios * 3. Pagination and Sorting Tests * 4. Error Handling and Edge Cases * 5. Performance and Resource Management * 6. Integration and Data Consistency Tests * 7. Authentication and Security Tests */ import { strictEqual, ok } from 'assert'; import { describe, it, before, after, beforeEach } from 'node:test'; import { connect } from 'mcp-aegis'; describe('search_system_object_attribute_groups - Full Mode Comprehensive Tests', () => { let client; const configPath = './aegis.config.with-dw.json'; before(async () => { client = await connect(configPath); }); after(async () => { if (client?.connected) { await client.disconnect(); } }); beforeEach(() => { // CRITICAL: Clear all buffers to prevent leaking into next tests if (client?.clearAllBuffers) { client.clearAllBuffers(); } }); // Enhanced helper functions for complex validations function assertValidMCPResponse(result) { ok(result.content, 'Should have content'); ok(Array.isArray(result.content), 'Content should be array'); strictEqual(typeof result.isError, 'boolean', 'isError should be boolean'); } function getTextContent(result) { assertValidMCPResponse(result); const textContent = result.content.find(c => c.type === 'text'); ok(textContent, 'Should have text content'); return textContent.text; } async function callTool(params) { const result = await client.callTool('search_system_object_attribute_groups', params); assertValidMCPResponse(result); return result; } describe('1. Basic Functionality Tests', () => { it('should be available in full mode', async () => { const tools = await client.listTools(); const tool = tools.find(t => t.name === 'search_system_object_attribute_groups'); ok(tool, 'Tool should be available in full mode'); strictEqual(tool.name, 'search_system_object_attribute_groups'); ok(tool.description.includes('Search attribute groups'), 'Tool should have proper description'); }); it('should return attribute groups for Product object type', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 10 } }); const text = getTextContent(result); ok(text.includes('object_attribute_group'), 'Should mention object_attribute_group in response'); }); it('should return attribute groups for SitePreferences object type', async () => { const result = await callTool({ objectType: 'SitePreferences', searchRequest: { query: { match_all_query: {} }, count: 5 } }); const text = getTextContent(result); ok(text.includes('SitePreferences'), 'Should mention SitePreferences'); }); it('should handle Customer object type', async () => { const result = await callTool({ objectType: 'Customer', searchRequest: { query: { match_all_query: {} }, count: 5 } }); const text = getTextContent(result); ok(text.length > 0, 'Should have some response content'); }); }); describe('2. Complex Query Scenarios', () => { it('should handle text_query for searching by display name', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { text_query: { fields: ['display_name', 'description'], search_phrase: 'product' } }, count: 5 } }); const text = getTextContent(result); ok(text.length > 0, 'Should have response content'); }); it('should handle term_query for exact field matching', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { term_query: { fields: ['internal'], operator: 'is', values: ['false'] } }, count: 10 } }); const text = getTextContent(result); ok(text.length > 0, 'Should have response content'); }); it('should handle complex bool_query with multiple conditions', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { bool_query: { must: [ { term_query: { fields: ['internal'], operator: 'is', values: ['false'] } } ], must_not: [ { text_query: { fields: ['id'], search_phrase: 'system' } } ] } }, count: 5 } }); const text = getTextContent(result); ok(text.length > 0, 'Should have response content'); }); }); describe('3. Pagination and Sorting Tests', () => { it('should handle pagination with start and count parameters', async () => { const result1 = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, start: 0, count: 2 } }); const result2 = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, start: 2, count: 2 } }); const text1 = getTextContent(result1); const text2 = getTextContent(result2); // Pages should be different (unless there are very few groups) if (text1.includes('attribute groups') && text2.includes('attribute groups')) { // Both pages have data, they might be different or the same if limited data ok(true, 'Pagination works correctly'); } }); it('should handle sorting by different fields', async () => { const ascResult = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, sorts: [{ field: 'id', sort_order: 'asc' }], count: 5 } }); const descResult = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, sorts: [{ field: 'id', sort_order: 'desc' }], count: 5 } }); const ascText = getTextContent(ascResult); const descText = getTextContent(descResult); // Both should have content ok(ascText.length > 0, 'Ascending sort should return data'); ok(descText.length > 0, 'Descending sort should return data'); }); it('should handle multiple sort criteria', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, sorts: [ { field: 'internal', sort_order: 'asc' }, { field: 'position', sort_order: 'desc' } ], count: 5 } }); const text = getTextContent(result); ok(text.length > 0, 'Should return data'); }); }); describe('4. Error Handling and Edge Cases', () => { it('should handle invalid object type gracefully', async () => { const result = await callTool({ objectType: 'InvalidObjectType', searchRequest: { query: { match_all_query: {} } } }); const text = getTextContent(result).toLowerCase(); ok( text.includes('error') || text.includes('not found') || text.includes('no') || text.includes('invalid') || text.includes('empty') || text.includes('0'), 'Should handle invalid object type appropriately' ); }); it('should handle missing required parameters', async () => { try { await client.callTool('search_system_object_attribute_groups', { searchRequest: { query: { match_all_query: {} } } // Missing objectType }); ok(false, 'Should have thrown an error for missing objectType'); } catch (error) { ok(error.message.includes('objectType') || error.message.includes('required'), 'Error should mention missing objectType'); } }); it('should handle malformed query structures', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { text_query: { // Missing required fields search_phrase: 'test' } } } }); const text = getTextContent(result); ok(text.length > 0, 'Should have some response'); }); it('should handle empty search results', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { text_query: { fields: ['id'], search_phrase: 'zzz_nonexistent_group_name_xyz' } } } }); const text = getTextContent(result).toLowerCase(); ok( text.includes('no') || text.includes('empty') || text.includes('not found') || text.includes('0') || text.includes('none'), 'Should indicate no results found' ); }); it('should handle very large count parameters', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 1000 // Very large count } }); const text = getTextContent(result); ok(text.length > 0, 'Should return data'); }); }); describe('5. Performance and Resource Management Tests', () => { it('should respond within reasonable time for simple queries', async () => { const startTime = Date.now(); const result = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 5 } }); const duration = Date.now() - startTime; ok(result.content, 'Should return result'); ok(duration < 5000, `Simple query should complete within 5 seconds, took ${duration}ms`); }); it('should handle concurrent requests efficiently', async () => { const startTime = Date.now(); const promises = Array.from({ length: 3 }, (_, i) => callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, start: i * 2, count: 2 } }) ); const results = await Promise.all(promises); const duration = Date.now() - startTime; ok(results.length === 3, 'Should handle all concurrent requests'); results.forEach((result, index) => { ok(result.content, `Request ${index} should have content`); }); ok(duration < 10000, `Concurrent requests should complete within 10 seconds, took ${duration}ms`); }); it('should handle memory efficiently with large result sets', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 200 // Large result set } }); const text = getTextContent(result); ok(text.length > 0, 'Should have response content'); ok(text.length < 1000000, 'Response should be reasonable in size (< 1MB)'); }); }); describe('6. Integration and Data Consistency Tests', () => { it('should return consistent results for repeated identical queries', async () => { const queryParams = { objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 5, sorts: [{ field: 'id', sort_order: 'asc' }] } }; const result1 = await callTool(queryParams); const result2 = await callTool(queryParams); const text1 = getTextContent(result1); const text2 = getTextContent(result2); // Results should be identical for same query strictEqual(text1, text2, 'Repeated identical queries should return same results'); }); it('should validate select parameter functionality', async () => { const fullResult = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 3, select: '(**)' } }); const limitedResult = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 3, select: '(data.(id))' } }); const fullText = getTextContent(fullResult); const limitedText = getTextContent(limitedResult); // Both should have content but limited might be shorter ok(fullText.length > 0, 'Full select should return data'); ok(limitedText.length > 0, 'Limited select should return data'); }); it('should handle cross-object type comparisons', async () => { const productResult = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 3 } }); const sitePrefsResult = await callTool({ objectType: 'SitePreferences', searchRequest: { query: { match_all_query: {} }, count: 3 } }); const productText = getTextContent(productResult); const sitePrefsText = getTextContent(sitePrefsResult); // Results should be different (different object types should have different groups) ok(productText.includes('Product') || productText.includes('attribute'), 'Product result should be relevant to products'); ok(sitePrefsText.includes('SitePreferences') || sitePrefsText.includes('attribute'), 'SitePreferences result should be relevant to site preferences'); }); }); describe('7. Authentication and Security Tests', () => { it('should require valid OCAPI credentials', async () => { // This test verifies that the tool properly uses authentication // The fact that we can call the tool successfully means auth is working const result = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 1 } }); const text = getTextContent(result).toLowerCase(); // Should not indicate authentication errors ok(!text.includes('unauthorized') && !text.includes('authentication'), 'Should not have authentication errors with valid credentials'); }); it('should handle input sanitization properly', async () => { // Test with potentially problematic input characters const result = await callTool({ objectType: 'Product', searchRequest: { query: { text_query: { fields: ['id'], search_phrase: '<script>alert("test")</script>' } }, count: 1 } }); const text = getTextContent(result); // The query echo should be properly escaped (showing backslashes for quotes) // and should not contain executable script content in the actual data hits ok(text.includes('\\"test\\"'), 'Should properly escape quotes in query echo'); // Check that the hits section doesn't contain the script content const jsonResponse = JSON.parse(text); if (jsonResponse.hits && jsonResponse.hits.length > 0) { const hitsText = JSON.stringify(jsonResponse.hits); ok(!hitsText.includes('<script>'), 'Actual data hits should not contain script content'); } }); it('should respect OCAPI rate limiting and security constraints', async () => { // Make multiple rapid requests to test rate limiting handling const promises = Array.from({ length: 5 }, () => callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 1 } }) ); try { const results = await Promise.all(promises); // All requests should succeed or fail gracefully results.forEach((result, index) => { ok(result.content, `Request ${index} should have content or proper error handling`); }); } catch (error) { // If rate limited, should fail gracefully ok(error.message.includes('rate') || error.message.includes('limit') || error.message.includes('too many'), 'Rate limiting should be handled gracefully'); } }); }); describe('8. Edge Case and Robustness Tests', () => { it('should handle extremely specific search criteria', async () => { const result = await callTool({ objectType: 'Product', searchRequest: { query: { bool_query: { must: [ { term_query: { fields: ['internal'], operator: 'is', values: ['false'] } }, { text_query: { fields: ['display_name'], search_phrase: 'custom' } } ] } }, count: 1 } }); const text = getTextContent(result); ok(text.length > 0, 'Should have response content'); }); it('should handle boundary values for pagination', async () => { // Test with start = 0 const zeroStart = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, start: 0, count: 1 } }); // Test with count = 1 const minCount = await callTool({ objectType: 'Product', searchRequest: { query: { match_all_query: {} }, start: 0, count: 1 } }); ok(zeroStart.content, 'Should handle start=0'); ok(minCount.content, 'Should handle count=1'); }); it('should maintain consistent response format across different scenarios', async () => { const scenarios = [ { name: 'match_all', params: { objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 2 } } }, { name: 'text_search', params: { objectType: 'Product', searchRequest: { query: { text_query: { fields: ['id'], search_phrase: 'product' } }, count: 2 } } }, { name: 'with_sorting', params: { objectType: 'Product', searchRequest: { query: { match_all_query: {} }, sorts: [{ field: 'id', sort_order: 'asc' }], count: 2 } } } ]; for (const scenario of scenarios) { const result = await callTool(scenario.params); assertValidMCPResponse(result); const textContent = result.content.find(c => c.type === 'text'); ok(textContent, `${scenario.name} should have text content`); ok(typeof textContent.text === 'string', `${scenario.name} should have string text`); } }); }); }); ``` -------------------------------------------------------------------------------- /tests/referenced-types-extractor.test.ts: -------------------------------------------------------------------------------- ```typescript import { ReferencedTypesExtractor } from '../src/clients/docs/referenced-types-extractor.js'; describe('ReferencedTypesExtractor', () => { describe('extractReferencedTypes', () => { it('should extract types from property definitions', () => { const content = ` # Class Product ## Properties ### price **Type:** Money The price of the product. ### category **Type:** Category The product category. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); expect(result).toContain('Money'); expect(result).toContain('Category'); expect(result).toHaveLength(2); }); it('should extract return types from method signatures', () => { const content = ` # Class Product ## Methods ### getPrice(): Money Returns the product price. ### getCategory(): Category Returns the product category. ### getName(): String Returns the product name. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); expect(result).toContain('Money'); expect(result).toContain('Category'); expect(result).toContain('String'); expect(result).toHaveLength(3); }); it('should extract parameter types from method signatures', () => { const content = ` # Class Product ## Methods ### setPrice(price: Money): void Sets the product price. ### addToCategory(category: Category, primary: Boolean): void Adds product to category. ### updateInventory(record: InventoryRecord, quantity: Number): Boolean Updates inventory record. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); expect(result).toContain('Money'); expect(result).toContain('Category'); expect(result).toContain('Boolean'); expect(result).toContain('InventoryRecord'); expect(result).toContain('Number'); expect(result).toHaveLength(5); }); it('should extract types with dots (fully qualified names)', () => { const content = ` # Class Product ## Properties ### site **Type:** dw.system.Site The site this product belongs to. ## Methods ### getCustomer(): dw.customer.Customer Returns the customer. ### processOrder(order: dw.order.Order): dw.system.Status Processes an order. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); expect(result).toContain('dw.system.Site'); expect(result).toContain('dw.customer.Customer'); expect(result).toContain('dw.order.Order'); expect(result).toContain('dw.system.Status'); expect(result).toHaveLength(4); }); it('should handle complex method signatures with multiple parameters', () => { const content = ` # Class OrderProcessor ## Methods ### processPayment(order: Order, payment: PaymentInstrument, amount: Money): PaymentStatus Processes payment for an order. ### createShipment(order: Order, items: Collection, address: OrderAddress): Shipment Creates a shipment. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); expect(result).toContain('Order'); expect(result).toContain('PaymentInstrument'); expect(result).toContain('Money'); expect(result).toContain('PaymentStatus'); expect(result).toContain('Collection'); expect(result).toContain('OrderAddress'); expect(result).toContain('Shipment'); expect(result).toHaveLength(7); }); it('should ignore primitive types and lowercase types', () => { const content = ` # Class Product ## Properties ### name **Type:** string Product name. ### active **Type:** boolean Whether product is active. ### count **Type:** number Product count. ### data **Type:** object Product data. ## Methods ### isValid(): boolean Returns validity. ### process(data: object, flag: boolean): string Processes data. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); // Should not include primitive types (string, boolean, number, object) expect(result).not.toContain('string'); expect(result).not.toContain('boolean'); expect(result).not.toContain('number'); expect(result).not.toContain('object'); expect(result).toHaveLength(0); }); it('should handle mixed case and special formatting', () => { const content = ` # Class ProductManager ## Properties ### defaultCategory **Type:** Category Default category with extra spaces. ### primarySite **Type:**dw.system.Site Site without space after colon. ## Methods ### getCollection(): Collection Returns collection with extra spaces. ### updateStatus( status: ProductStatus ): Boolean Method with spaced parameters. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); expect(result).toContain('Category'); expect(result).toContain('dw.system.Site'); expect(result).toContain('Collection'); expect(result).toContain('ProductStatus'); expect(result).toContain('Boolean'); expect(result).toHaveLength(5); }); it('should handle empty content', () => { const result = ReferencedTypesExtractor.extractReferencedTypes(''); expect(result).toEqual([]); }); it('should handle content with no type references', () => { const content = ` # Class Product This is a product class without any type references. It has some text but no property types or method signatures. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); expect(result).toEqual([]); }); it('should handle malformed content gracefully', () => { const content = ` # Class Product **Type:** ### method(): Returns something. ### invalid(: Missing): Malformed signature. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); // The regex will match "Missing" from the parameter, which starts with uppercase expect(result).toContain('Missing'); expect(result).toHaveLength(1); }); it('should deduplicate repeated type references', () => { const content = ` # Class Product ## Properties ### price **Type:** Money ### discountPrice **Type:** Money ## Methods ### getPrice(): Money Returns price. ### setPrice(amount: Money): void Sets price. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); expect(result).toContain('Money'); expect(result.filter(type => type === 'Money')).toHaveLength(1); expect(result).toHaveLength(1); }); }); describe('isSFCCType', () => { it('should identify uppercase types as SFCC types', () => { // Access private method through any for testing const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; expect(isSFCCType('Product')).toBe(true); expect(isSFCCType('Money')).toBe(true); expect(isSFCCType('Category')).toBe(true); expect(isSFCCType('Boolean')).toBe(true); expect(isSFCCType('String')).toBe(true); expect(isSFCCType('Number')).toBe(true); }); it('should identify dotted types as SFCC types', () => { const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; expect(isSFCCType('dw.catalog.Product')).toBe(true); expect(isSFCCType('dw.system.Site')).toBe(true); expect(isSFCCType('dw.customer.Customer')).toBe(true); expect(isSFCCType('dw.order.Order')).toBe(true); }); it('should reject lowercase primitive types', () => { const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; expect(isSFCCType('string')).toBe(false); expect(isSFCCType('boolean')).toBe(false); expect(isSFCCType('number')).toBe(false); expect(isSFCCType('object')).toBe(false); expect(isSFCCType('function')).toBe(false); expect(isSFCCType('undefined')).toBe(false); expect(isSFCCType('null')).toBe(false); }); it('should reject types starting with lowercase', () => { const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; expect(isSFCCType('productType')).toBe(false); expect(isSFCCType('categoryId')).toBe(false); expect(isSFCCType('data')).toBe(false); expect(isSFCCType('value')).toBe(false); }); it('should handle edge cases', () => { const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType; expect(isSFCCType('')).toBe(false); expect(isSFCCType('A')).toBe(true); expect(isSFCCType('a')).toBe(false); expect(isSFCCType('1Product')).toBe(false); // starts with number expect(isSFCCType('_Product')).toBe(false); // starts with underscore }); }); describe('filterCircularReferences', () => { it('should filter out exact class name matches', () => { const referencedTypes = ['Product', 'Category', 'Money', 'Product']; const currentClassName = 'Product'; const result = ReferencedTypesExtractor.filterCircularReferences( referencedTypes, currentClassName, ); expect(result).toContain('Category'); expect(result).toContain('Money'); expect(result).not.toContain('Product'); expect(result).toHaveLength(2); }); it('should filter out fully qualified class name matches', () => { const referencedTypes = [ 'dw.catalog.Product', 'dw.system.Site', 'Category', 'Money', ]; const currentClassName = 'Product'; const result = ReferencedTypesExtractor.filterCircularReferences( referencedTypes, currentClassName, ); expect(result).toContain('dw.system.Site'); expect(result).toContain('Category'); expect(result).toContain('Money'); expect(result).not.toContain('dw.catalog.Product'); expect(result).toHaveLength(3); }); it('should preserve types that do not create circular references', () => { const referencedTypes = [ 'Category', 'Money', 'ProductVariant', // Different from Product 'dw.system.Site', 'Boolean', ]; const currentClassName = 'Product'; const result = ReferencedTypesExtractor.filterCircularReferences( referencedTypes, currentClassName, ); expect(result).toEqual([ 'Category', 'Money', 'ProductVariant', 'dw.system.Site', 'Boolean', ]); expect(result).toHaveLength(5); }); it('should handle empty input arrays', () => { const result = ReferencedTypesExtractor.filterCircularReferences([], 'Product'); expect(result).toEqual([]); }); it('should handle case where all types are circular references', () => { const referencedTypes = [ 'Product', 'dw.catalog.Product', 'some.namespace.Product', ]; const currentClassName = 'Product'; const result = ReferencedTypesExtractor.filterCircularReferences( referencedTypes, currentClassName, ); expect(result).toEqual([]); }); it('should be case sensitive', () => { const referencedTypes = ['product', 'PRODUCT', 'Product']; const currentClassName = 'Product'; const result = ReferencedTypesExtractor.filterCircularReferences( referencedTypes, currentClassName, ); expect(result).toContain('product'); expect(result).toContain('PRODUCT'); expect(result).not.toContain('Product'); expect(result).toHaveLength(2); }); it('should handle complex namespace scenarios', () => { const referencedTypes = [ 'dw.catalog.Category', 'dw.order.OrderItem', // Different class 'com.custom.Product', // Different namespace but same class 'Product', // Exact match ]; const currentClassName = 'Product'; const result = ReferencedTypesExtractor.filterCircularReferences( referencedTypes, currentClassName, ); expect(result).toContain('dw.catalog.Category'); expect(result).toContain('dw.order.OrderItem'); expect(result).not.toContain('com.custom.Product'); // Should be filtered expect(result).not.toContain('Product'); // Should be filtered expect(result).toHaveLength(2); }); }); describe('extractFilteredReferencedTypes', () => { it('should extract and filter types in one operation', () => { const content = ` # Class Product ## Properties ### price **Type:** Money ### category **Type:** Category ### relatedProduct **Type:** Product ## Methods ### getPrice(): Money Returns price. ### getRelatedProduct(): Product Returns related product. ### createProduct(name: String): Product Creates a new product. `; const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( content, 'Product', ); expect(result).toContain('Money'); expect(result).toContain('Category'); expect(result).toContain('String'); expect(result).not.toContain('Product'); // Should be filtered out expect(result).toHaveLength(3); }); it('should handle complex scenarios with multiple filtering needs', () => { const content = ` # Class OrderProcessor ## Properties ### defaultOrder **Type:** dw.order.Order ### processor **Type:** OrderProcessor ## Methods ### processOrder(order: dw.order.Order): OrderProcessor Processes an order. ### createProcessor(): OrderProcessor Creates processor instance. ### validatePayment(payment: PaymentInstrument): Boolean Validates payment. `; const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( content, 'OrderProcessor', ); expect(result).toContain('dw.order.Order'); expect(result).toContain('PaymentInstrument'); expect(result).toContain('Boolean'); expect(result).not.toContain('OrderProcessor'); // Should be filtered out expect(result).toHaveLength(3); }); it('should work with empty content', () => { const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( '', 'Product', ); expect(result).toEqual([]); }); it('should work when no types need filtering', () => { const content = ` # Class Product ## Properties ### price **Type:** Money ### category **Type:** Category ## Methods ### getInventory(): InventoryRecord Returns inventory. `; const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( content, 'Product', ); expect(result).toContain('Money'); expect(result).toContain('Category'); expect(result).toContain('InventoryRecord'); expect(result).toHaveLength(3); }); it('should work when all types need filtering', () => { const content = ` # Class Product ## Properties ### self **Type:** Product ## Methods ### getSelf(): Product Returns self. ### createProduct(): dw.catalog.Product Creates product. `; const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( content, 'Product', ); expect(result).toEqual([]); }); }); describe('integration tests', () => { it('should handle realistic SFCC class documentation', () => { const content = ` # Class dw.catalog.Product The Product class represents a product in the catalog. ## Properties ### ID **Type:** String The product ID. ### name **Type:** String The product name. ### primaryCategory **Type:** Category The primary category for this product. ### priceModel **Type:** ProductPriceModel The price model for this product. ### availabilityModel **Type:** ProductAvailabilityModel The availability model for this product. ## Methods ### getID(): String Returns the product ID. ### getName(): String Returns the product name. ### getPrimaryCategory(): Category Returns the primary category. ### getPriceModel(): ProductPriceModel Returns the price model. ### getAvailabilityModel(): ProductAvailabilityModel Returns the availability model. ### setName(name: String): void Sets the product name. ### assignToCategory(category: Category, primary: Boolean): void Assigns the product to a category. ### getVariationModel(): ProductVariationModel Returns the variation model. ### isVariant(): Boolean Returns true if this is a variant product. ### getMasterProduct(): Product Returns the master product if this is a variant. ### getVariants(): Collection Returns all variants of this master product. `; const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( content, 'Product', ); expect(result).toContain('String'); expect(result).toContain('Category'); expect(result).toContain('ProductPriceModel'); expect(result).toContain('ProductAvailabilityModel'); expect(result).toContain('Boolean'); expect(result).toContain('ProductVariationModel'); expect(result).toContain('Collection'); expect(result).not.toContain('Product'); // Should be filtered out expect(result).toHaveLength(7); }); it('should handle documentation with inheritance information', () => { const content = ` # Class ProductVariant extends Product A product variant inherits from Product. ## Properties ### masterProduct **Type:** Product The master product. ### variationAttributes **Type:** Map The variation attributes. ## Methods ### getMasterProduct(): Product Returns the master product. ### getVariationValue(attribute: ProductAttribute): ProductAttributeValue Returns variation value. ### isOnline(): Boolean Checks if variant is online. `; const result = ReferencedTypesExtractor.extractFilteredReferencedTypes( content, 'ProductVariant', ); expect(result).toContain('Map'); expect(result).toContain('ProductAttribute'); expect(result).toContain('ProductAttributeValue'); expect(result).toContain('Boolean'); expect(result).toContain('Product'); // Will be included since it's extracted from content expect(result).not.toContain('ProductVariant'); // Should be filtered as self-reference expect(result).toHaveLength(5); }); }); describe('error handling and edge cases', () => { it('should handle null input gracefully', () => { expect(() => { ReferencedTypesExtractor.extractReferencedTypes(null as any); }).toThrow(); }); it('should handle undefined input gracefully', () => { expect(() => { ReferencedTypesExtractor.extractReferencedTypes(undefined as any); }).toThrow(); }); it('should handle very large content efficiently', () => { const largeContent = ` # Class Product ## Properties ### prop1 **Type:** Type1 `.repeat(1000); const result = ReferencedTypesExtractor.extractReferencedTypes(largeContent); expect(result).toContain('Type1'); expect(result).toHaveLength(1); }); it('should handle content with special characters', () => { const content = ` # Class Product ## Properties ### price€ **Type:** Money€ European price. ### nameÜnicode **Type:** String Unicode name. ## Methods ### getPrice€(): Money€ Returns European price. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); // The current regex pattern [A-Za-z0-9.] doesn't include Unicode characters // So Money€ will be extracted as just "Money" expect(result).toContain('Money'); // Money€ gets truncated to Money expect(result).toContain('String'); expect(result).toHaveLength(2); }); it('should handle malformed markdown gracefully', () => { const content = ` # Class Product **Type:** Category This is not a proper property definition. ### method(: Money Malformed method signature. **Type:** Empty type. ### valid **Type:** ValidType This should work. `; const result = ReferencedTypesExtractor.extractReferencedTypes(content); // The extractor will find: // - "Category" from "**Type:** Category" (matches property pattern) // - "Money" from "method(: Money" (matches return type pattern) // - "ValidType" from the proper property definition expect(result).toContain('Category'); expect(result).toContain('Money'); expect(result).toContain('ValidType'); expect(result).toHaveLength(3); }); }); }); ``` -------------------------------------------------------------------------------- /tests/mcp/node/search-sfcc-methods.docs-only.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript /** * Programmatic tests for search_sfcc_methods tool * * These tests provide advanced verification capabilities beyond YAML pattern matching, * including performance monitoring, dynamic validation, error categorization, * comprehensive response structure analysis, and method signature validation. * * Response format discovered via aegis query: * - Success: { content: [{ type: "text", text: "[{\"className\": \"...\", \"method\": {...}}, ...]" }] } * - Empty: { content: [{ type: "text", text: "[]" }] } * - Error: { content: [{ type: "text", text: "Error: ..." }], isError: true } * * Method object structure: * { * "className": "dw_util.Calendar", * "method": { * "name": "get", * "signature": "get(field : Number) : Number", * "description": "Returns the value of the given calendar field." * } * } */ import { test, describe, before, after, beforeEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { connect } from 'mcp-aegis'; /** * Performance monitoring utility class */ /** * Method signature analysis utility class */ class MethodSignatureAnalyzer { constructor() { this.patterns = { staticMethod: /^static\s+/, returnType: /:\s*([A-Za-z0-9_[\]]+)\s*$/, parameters: /\(([^)]*)\)/, methodName: /^(?:static\s+)?([A-Za-z_][A-Za-z0-9_]*)/ }; } analyzeSignature(signature) { return { isStatic: this.patterns.staticMethod.test(signature), returnType: this.extractReturnType(signature), parameters: this.extractParameters(signature), methodName: this.extractMethodName(signature), isValid: this.validateSignature(signature) }; } extractReturnType(signature) { const match = signature.match(this.patterns.returnType); return match ? match[1] : null; } extractParameters(signature) { const match = signature.match(this.patterns.parameters); if (!match || !match[1].trim()) return []; return match[1].split(',').map(param => { const parts = param.trim().split(/\s*:\s*/); return { name: parts[0]?.trim() || '', type: parts[1]?.trim() || '' }; }); } extractMethodName(signature) { const match = signature.match(this.patterns.methodName); return match ? match[1] : null; } validateSignature(signature) { // Basic validation - should have method name, parentheses, and return type return signature.includes('(') && signature.includes(')') && signature.includes(':') && signature.trim().length > 0; } categorizeMethod(methodData) { const { signature } = methodData.method; const analysis = this.analyzeSignature(signature); const categories = []; if (analysis.isStatic) categories.push('static'); if (analysis.methodName?.startsWith('get')) categories.push('getter'); if (analysis.methodName?.startsWith('set')) categories.push('setter'); if (analysis.methodName?.startsWith('is') || analysis.methodName?.startsWith('has')) categories.push('boolean'); if (analysis.returnType === 'void') categories.push('void'); if (analysis.parameters.length === 0) categories.push('parameterless'); if (analysis.parameters.length > 3) categories.push('complex'); return categories; } } describe('search_sfcc_methods Programmatic Tests', () => { let client; const signatureAnalyzer = new MethodSignatureAnalyzer(); before(async () => { client = await connect('./aegis.config.docs-only.json'); }); after(async () => { if (client?.connected) { await client.disconnect(); } }); beforeEach(() => { // CRITICAL: Clear all buffers to prevent test interference client.clearAllBuffers(); // Recommended - comprehensive protection }); describe('Protocol Compliance', () => { test('should be properly connected to MCP server', async () => { assert.ok(client.connected, 'Client should be connected'); }); test('should have search_sfcc_methods tool available', async () => { const tools = await client.listTools(); const searchTool = tools.find(tool => tool.name === 'search_sfcc_methods'); assert.ok(searchTool, 'search_sfcc_methods tool should be available'); assert.equal(searchTool.name, 'search_sfcc_methods'); assert.ok(searchTool.description, 'Tool should have description'); assert.ok(searchTool.inputSchema, 'Tool should have input schema'); assert.equal(searchTool.inputSchema.type, 'object'); assert.ok(searchTool.inputSchema.properties.methodName, 'Tool should require methodName parameter'); }); }); describe('Response Structure Validation', () => { test('should return properly structured MCP response for valid method search', async () => { const result = await client.callTool('search_sfcc_methods', { methodName: 'get' }); // Validate MCP response structure assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should not be an error response'); assert.equal(result.content.length, 1, 'Should have exactly one content item'); assert.equal(result.content[0].type, 'text', 'Content should be text type'); // Validate JSON array structure const methodArray = parseMethodArray(result.content[0].text); assert.ok(Array.isArray(methodArray), 'Response should contain valid JSON array'); assert.ok(methodArray.length > 0, 'Should return at least one method for get query'); // Validate method object structure methodArray.forEach(methodData => { assert.equal(typeof methodData, 'object', 'Each method should be an object'); assert.ok(methodData.className, 'Method should have className property'); assert.ok(methodData.method, 'Method should have method property'); assert.equal(typeof methodData.className, 'string', 'className should be string'); assert.equal(typeof methodData.method, 'object', 'method should be object'); // Validate method object properties assert.ok(methodData.method.name, 'Method should have name'); assert.ok(methodData.method.signature, 'Method should have signature'); assert.ok(methodData.method.description, 'Method should have description'); assert.equal(typeof methodData.method.name, 'string', 'Method name should be string'); assert.equal(typeof methodData.method.signature, 'string', 'Method signature should be string'); assert.equal(typeof methodData.method.description, 'string', 'Method description should be string'); }); // Performance validation (lenient for CI environments) }); test('should return empty array for no matches', async () => { const result = await client.callTool( 'search_sfcc_methods', { methodName: 'zzznothingfound' } ); assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should not be an error response'); const methodArray = parseMethodArray(result.content[0].text); assert.ok(Array.isArray(methodArray), 'Response should be valid JSON array'); assert.equal(methodArray.length, 0, 'Should return empty array for no matches'); // Performance should be reasonable for no results (lenient for CI) }); test('should return error response for invalid parameters', async () => { const result = await client.callTool('search_sfcc_methods', { methodName: '' } ); assertValidMCPResponse(result); assert.equal(result.isError, true, 'Should be an error response'); assert.ok(result.content[0].text.includes('Error:'), 'Should contain error message'); assert.ok(result.content[0].text.includes('non-empty string'), 'Should specify validation requirement'); // Error responses should be reasonably fast (CI-friendly) }); }); describe('Method Search Functionality', () => { const commonMethodQueries = [ { query: 'get', expectedMin: 50, category: 'getter' }, { query: 'set', expectedMin: 10, category: 'setter' }, { query: 'create', expectedMin: 5, category: 'factory' }, { query: 'toString', expectedMin: 20, category: 'conversion' }, { query: 'getValue', expectedMin: 10, category: 'accessor' }, { query: 'getName', expectedMin: 5, category: 'accessor' } ]; commonMethodQueries.forEach(({ query, expectedMin, category }) => { test(`should find relevant methods for ${category} query: "${query}"`, async () => { const result = await client.callTool( 'search_sfcc_methods', { methodName: query } ); assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should not be an error'); const methodArray = parseMethodArray(result.content[0].text); assert.ok(methodArray.length >= expectedMin, `Should find at least ${expectedMin} methods for "${query}", found ${methodArray.length}`); // Validate relevance - all method names should contain the query term methodArray.forEach(methodData => { const methodName = methodData.method.name.toLowerCase(); const lowerQuery = query.toLowerCase(); assert.ok(methodName.includes(lowerQuery), `Method "${methodData.method.name}" should contain query term "${query}"`); }); // Validate class name format methodArray.forEach(methodData => { assert.ok( methodData.className.startsWith('dw_') || methodData.className.startsWith('TopLevel.') || methodData.className.startsWith('best-practices.') || methodData.className.startsWith('sfra.'), `Class name "${methodData.className}" should start with recognized namespace` ); }); }); }); }); describe('Method Signature Analysis', () => { test('should return valid method signatures for all results', async () => { const result = await client.callTool('search_sfcc_methods', { methodName: 'get' }); assertValidMCPResponse(result); const methodArray = parseMethodArray(result.content[0].text); methodArray.slice(0, 20).forEach(methodData => { // Test first 20 for performance const analysis = signatureAnalyzer.analyzeSignature(methodData.method.signature); assert.ok(analysis.isValid, `Method signature "${methodData.method.signature}" should be valid`); assert.ok(analysis.methodName, `Should extract method name from "${methodData.method.signature}"`); assert.ok(analysis.returnType, `Should extract return type from "${methodData.method.signature}"`); // Method name in signature should match the method name property assert.equal(analysis.methodName, methodData.method.name, 'Method name in signature should match method.name property'); // Signature should be properly formatted assert.ok(methodData.method.signature.includes('('), 'Signature should contain opening parenthesis'); assert.ok(methodData.method.signature.includes(')'), 'Signature should contain closing parenthesis'); assert.ok(methodData.method.signature.includes(':'), 'Signature should contain return type separator'); }); }); test('should categorize methods correctly by signature patterns', async () => { const result = await client.callTool('search_sfcc_methods', { methodName: 'get' }); assertValidMCPResponse(result); const methodArray = parseMethodArray(result.content[0].text); const categoryCounts = { static: 0, getter: 0, parameterless: 0, complex: 0 }; methodArray.slice(0, 50).forEach(methodData => { const categories = signatureAnalyzer.categorizeMethod(methodData); categories.forEach(category => { if (Object.prototype.hasOwnProperty.call(categoryCounts, category)) { categoryCounts[category]++; } }); }); // Should find a good mix of method types assert.ok(categoryCounts.getter > 10, 'Should find plenty of getter methods'); // Static methods might not be common in 'get' search, so make this more flexible assert.ok(categoryCounts.static >= 0, 'Static method count should be non-negative'); assert.ok(categoryCounts.parameterless > 5, 'Should find parameterless methods'); assert.ok(Object.keys(categoryCounts).length > 0, 'Should have categorized methods'); }); }); describe('Edge Case Validation', () => { const edgeCases = [ { methodName: 'A', description: 'single character' }, { methodName: 'get', description: 'common method prefix' }, { methodName: 'GET', description: 'uppercase query' }, { methodName: 'getValue', description: 'compound method name' }, { methodName: '123', description: 'numeric query' }, { methodName: 'xyz_nonexistent_method', description: 'clearly non-existent method' } ]; edgeCases.forEach(({ methodName, description }) => { test(`should handle ${description} query: "${methodName}"`, async () => { const result = await client.callTool( 'search_sfcc_methods', { methodName } ); assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should not be an error for valid string'); const methodArray = parseMethodArray(result.content[0].text); assert.ok(Array.isArray(methodArray), 'Should return valid array'); // All results should have valid structure methodArray.forEach(methodData => { assert.ok(methodData.className, 'Should have className'); assert.ok(methodData.method, 'Should have method object'); assert.ok(methodData.method.name, 'Should have method name'); assert.ok(methodData.method.signature, 'Should have method signature'); assert.ok(methodData.method.description, 'Should have method description'); // Method name should contain the query (case insensitive) const methodNameLower = methodData.method.name.toLowerCase(); const queryLower = methodName.toLowerCase(); assert.ok(methodNameLower.includes(queryLower), `Method "${methodData.method.name}" should contain query "${methodName}"`); }); // Performance should be reasonable for CI environments }); }); }); describe('Error Handling Validation', () => { const errorCases = [ { args: { methodName: '' }, description: 'empty method name' }, { args: {}, description: 'missing methodName parameter' }, { args: { methodName: ' ' }, description: 'whitespace-only method name' }, { args: { methodName: null }, description: 'null method name' }, { args: { methodName: 123 }, description: 'non-string method name (number)' }, { args: { methodName: true }, description: 'non-string method name (boolean)' }, { args: { methodName: [] }, description: 'non-string method name (array)' }, { args: { methodName: {} }, description: 'non-string method name (object)' } ]; errorCases.forEach(({ args, description }) => { test(`should return error for ${description}`, async () => { const result = await client.callTool( 'search_sfcc_methods', args ); assertValidMCPResponse(result); assert.equal(result.isError, true, 'Should be an error response'); assert.ok(result.content[0].text.includes('Error'), 'Should contain error message'); // Categorize error type const errorType = categorizeError(result.content[0].text); assert.ok(['validation', 'not_found', 'unknown'].includes(errorType), `Error should be categorized (got: ${errorType})`); // Error responses should be reasonably fast (CI-friendly) }); }); }); describe('Consistency and Reliability', () => { test('should return consistent results across multiple calls', async () => { const methodName = 'getValue'; const results = await Promise.all([ client.callTool('search_sfcc_methods', { methodName }), client.callTool('search_sfcc_methods', { methodName }), client.callTool('search_sfcc_methods', { methodName }) ]); // All results should be successful results.forEach(result => { assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should not be error'); }); // Parse arrays for comparison const arrays = results.map(result => parseMethodArray(result.content[0].text)); // All arrays should be identical assert.deepEqual(arrays[0], arrays[1], 'Results should be consistent across calls'); assert.deepEqual(arrays[1], arrays[2], 'Results should be consistent across calls'); }); test('should validate method data integrity and format', async () => { const result = await client.callTool('search_sfcc_methods', { methodName: 'toString' }); assertValidMCPResponse(result); const methodArray = parseMethodArray(result.content[0].text); methodArray.forEach(methodData => { // Validate method data integrity assert.ok(methodData.className.length > 3, `Class name "${methodData.className}" should be reasonable length`); assert.ok(methodData.className.length < 100, `Class name "${methodData.className}" should not be excessively long`); // Validate class name format assert.match(methodData.className, /^(dw_|TopLevel\.|best-practices\.|sfra\.)[a-zA-Z0-9_./-]+$/, `Class name "${methodData.className}" should follow valid pattern`); // Validate method name format assert.match(methodData.method.name, /^[a-zA-Z_][a-zA-Z0-9_]*$/, `Method name "${methodData.method.name}" should follow valid identifier pattern`); // Should not contain HTML or special characters assert.ok(!methodData.method.name.includes('<'), 'Method name should not contain HTML'); assert.ok(!methodData.method.name.includes('>'), 'Method name should not contain HTML'); // Description should be reasonable length assert.ok(methodData.method.description.length > 5, 'Description should not be too short'); assert.ok(methodData.method.description.length < 2000, 'Description should not be excessively long'); // Signature should contain the method name assert.ok(methodData.method.signature.includes(methodData.method.name), 'Signature should contain the method name'); }); }); }); }); // Helper functions /** * Validates that a response follows proper MCP structure */ function assertValidMCPResponse(result) { assert.ok(result.content, 'Response should have content property'); assert.ok(Array.isArray(result.content), 'Content should be an array'); assert.ok(result.content.length > 0, 'Content array should not be empty'); assert.equal(result.content[0].type, 'text', 'First content item should be text type'); assert.equal(typeof result.content[0].text, 'string', 'Text content should be a string'); // isError property should always be present and boolean assert.ok(Object.prototype.hasOwnProperty.call(result, 'isError'), 'isError property should always be present'); assert.equal(typeof result.isError, 'boolean', 'isError should be a boolean'); } /** * Parses the method array from the response text */ function parseMethodArray(text) { try { return JSON.parse(text); } catch { throw new Error(`Failed to parse method array from response: ${text}`); } } /** * Categorizes error messages by type */ function categorizeError(errorText) { const errorPatterns = [ { type: 'validation', keywords: ['required', 'invalid', 'missing', 'non-empty', 'string'] }, { type: 'not_found', keywords: ['not found', 'does not exist'] }, { type: 'permission', keywords: ['permission', 'unauthorized', 'forbidden'] }, { type: 'network', keywords: ['connection', 'timeout', 'unreachable'] } ]; const lowerText = errorText.toLowerCase(); for (const pattern of errorPatterns) { if (pattern.keywords.some(keyword => lowerText.includes(keyword))) { return pattern.type; } } return 'unknown'; } ``` -------------------------------------------------------------------------------- /tests/mcp/node/search-job-logs.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript import { test, describe, before, after, beforeEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { connect } from 'mcp-aegis'; describe('search_job_logs - Optimized Programmatic Tests', () => { let client; let discoveredJobNames = []; let discoveredPatterns = []; before(async () => { client = await connect('./aegis.config.with-dw.json'); // Discover available job names and patterns for advanced testing await discoverJobNames(); await discoverCommonPatterns(); }); after(async () => { if (client?.connected) { await client.disconnect(); } }); beforeEach(() => { // CRITICAL: Clear all buffers to prevent leaking into next tests client.clearAllBuffers(); // Recommended - comprehensive protection }); // Helper functions for dynamic discovery and complex validation async function discoverJobNames() { try { const result = await client.callTool('search_job_logs', { pattern: 'Executing', limit: 50 }); if (!result.isError && result.content?.[0]?.text) { const text = parseResponseText(result.content[0].text); // Extract job names from the beginning of each log entry (first bracket pair) const jobMatches = text.match(/^\[([A-Za-z][A-Za-z0-9_-]*)\]/gm) || []; discoveredJobNames = [...new Set(jobMatches.map(match => match.slice(1, -1)))]; } } catch (error) { console.warn('Could not discover job names:', error.message); } } async function discoverCommonPatterns() { const commonTerms = ['INFO', 'ERROR', 'step', 'completed', 'job', 'Executing']; for (const term of commonTerms) { try { const result = await client.callTool('search_job_logs', { pattern: term, limit: 1 }); if (!result.isError && result.content?.[0]?.text) { const text = parseResponseText(result.content[0].text); if (!text.includes('No matches found')) { discoveredPatterns.push(term); } } } catch (error) { console.warn(`Error testing pattern "${term}":`, error.message); } } } // Helper functions for complex validations function assertValidMCPResponse(result) { assert.ok(result.content, 'Should have content'); assert.ok(Array.isArray(result.content), 'Content should be array'); assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); } function parseResponseText(text) { // The response may come wrapped in quotes, so parse if needed return text.startsWith('"') && text.endsWith('"') ? JSON.parse(text) : text; } function assertSearchResultsFormat(result, pattern, expectedJobName = null) { assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should not be an error response'); assert.equal(result.content[0].type, 'text'); const text = parseResponseText(result.content[0].text); if (text.includes('No matches found')) { // Valid empty result case assert.ok(text.includes(`No matches found for "${pattern}"`), `Should indicate no matches for pattern "${pattern}"`); return { matchCount: 0, entries: [] }; } // Should contain found matches header if (expectedJobName) { assert.ok(text.includes(`Found`) && text.includes(`matches for "${pattern}"`), `Should contain found matches header for pattern "${pattern}"`); assert.ok(text.includes(`job: ${expectedJobName}`) || text.includes(`in job: ${expectedJobName}`), `Should indicate filtering by job "${expectedJobName}"`); } else { assert.ok(text.includes(`Found`) && text.includes(`matches for "${pattern}"`), `Should contain found matches header for pattern "${pattern}"`); } // Extract job log entries const entries = extractJobLogEntries(text); // Validate each entry has proper structure and contains pattern for (const entry of entries) { // Each entry should have job name in brackets assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()), `Entry should start with job name in brackets: "${entry.substring(0, 50)}..."`); // Should contain timestamp in GMT format assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(entry), `Entry should contain GMT timestamp: "${entry.substring(0, 100)}..."`); // Should contain the search pattern (case-insensitive check) assert.ok(entry.toLowerCase().includes(pattern.toLowerCase()), `Entry should contain pattern "${pattern}": "${entry.substring(0, 100)}..."`); // If filtering by job name, all entries should be from that job if (expectedJobName) { assert.ok(entry.includes(`[${expectedJobName}]`), `Entry should be from job "${expectedJobName}": "${entry.substring(0, 50)}..."`); } } // Extract match count from header const matchCountMatch = text.match(/Found (\d+) matches/); const matchCount = matchCountMatch ? parseInt(matchCountMatch[1]) : entries.length; return { matchCount, entries }; } function extractJobLogEntries(text) { // Split by double newlines and filter for entries that look like job logs const lines = text.split(/\n\n+/); return lines.filter(line => line.trim() && /^\[[\w\-_]+\]/.test(line.trim()) && /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(line) ); } function assertTimestampFormat(entries) { for (const entry of entries) { // Should contain valid timestamp format const timestampMatch = entry.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT)/); assert.ok(timestampMatch, `Entry should contain valid timestamp: "${entry.substring(0, 100)}..."`); // Validate timestamp is parseable const timestampStr = timestampMatch[1].replace(' GMT', 'Z'); const date = new Date(timestampStr); assert.ok(!isNaN(date.getTime()), `Timestamp should be valid date: "${timestampMatch[1]}"`); } } // === Dynamic Discovery and Validation Tests === describe('Dynamic Discovery and Validation', () => { test('should dynamically validate discovered job names', async () => { if (discoveredJobNames.length === 0) { console.warn('No job names discovered - skipping dynamic validation'); return; } // Test a sample of discovered job names (not all to avoid excessive testing) const sampleJobs = discoveredJobNames.slice(0, Math.min(3, discoveredJobNames.length)); for (const jobName of sampleJobs) { const result = await client.callTool('search_job_logs', { pattern: 'Executing', jobName: jobName, limit: 1 }); assertValidMCPResponse(result); assert.equal(result.isError, false, `Job "${jobName}" should be searchable`); const searchResults = assertSearchResultsFormat(result, 'Executing', jobName); // If we found entries, they should all be from the specified job if (searchResults.entries.length > 0) { for (const entry of searchResults.entries) { assert.ok(entry.includes(`[${jobName}]`), `Entry should be from job "${jobName}": "${entry.substring(0, 50)}..."`); } } } }); test('should dynamically test discovered patterns with complex validation', async () => { if (discoveredPatterns.length === 0) { console.warn('No patterns discovered - skipping dynamic pattern testing'); return; } for (const pattern of discoveredPatterns) { const result = await client.callTool('search_job_logs', { pattern: pattern, limit: 3 }); assertValidMCPResponse(result); assert.equal(result.isError, false, `Pattern "${pattern}" should be searchable`); const searchResults = assertSearchResultsFormat(result, pattern); if (searchResults.entries.length > 0) { // Validate complex content structure for each discovered pattern assertTimestampFormat(searchResults.entries); // Validate pattern appears in content with case-insensitive search for (const entry of searchResults.entries) { assert.ok(entry.toLowerCase().includes(pattern.toLowerCase()), `Entry should contain pattern "${pattern}" (case-insensitive): "${entry.substring(0, 100)}..."`); } } } }); test('should validate cross-job pattern distribution', async () => { if (discoveredJobNames.length < 2 || discoveredPatterns.length === 0) { console.warn('Insufficient data for cross-job validation - skipping'); return; } // Test if common patterns appear across multiple jobs const commonPattern = discoveredPatterns[0]; // Use first discovered pattern const jobResults = new Map(); // Search each job for the common pattern for (const jobName of discoveredJobNames.slice(0, 3)) { const result = await client.callTool('search_job_logs', { pattern: commonPattern, jobName: jobName, limit: 1 }); assertValidMCPResponse(result); const searchResults = assertSearchResultsFormat(result, commonPattern, jobName); jobResults.set(jobName, searchResults.matchCount); } // Should be able to search across multiple jobs successfully assert.ok(jobResults.size > 0, 'Should be able to search multiple jobs'); }); }); // === Complex Content and Format Validation === describe('Complex Content Validation', () => { test('should validate comprehensive job log entry structure', async () => { const result = await client.callTool('search_job_logs', { pattern: 'step', limit: 5 }); assertValidMCPResponse(result); const searchResults = assertSearchResultsFormat(result, 'step'); if (searchResults.entries.length > 0) { for (const entry of searchResults.entries) { // Complex structural validation assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()), `Entry should start with job name: "${entry.substring(0, 50)}..."`); // Validate timestamp format and parseability const timestampMatch = entry.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT)/); assert.ok(timestampMatch, `Entry should contain timestamp: "${entry.substring(0, 100)}..."`); const timestampStr = timestampMatch[1].replace(' GMT', 'Z'); const date = new Date(timestampStr); assert.ok(!isNaN(date.getTime()), `Timestamp should be parseable: "${timestampMatch[1]}"`); // Validate log level presence assert.ok(/\s(INFO|ERROR|WARN|DEBUG)\s/.test(entry), `Entry should contain log level: "${entry.substring(0, 100)}..."`); // Validate thread information assert.ok(/Thread|SystemJobThread/.test(entry), `Entry should contain thread info: "${entry.substring(0, 100)}..."`); } } }); test('should maintain consistent format across different search parameters', async () => { const testCombinations = [ { pattern: 'INFO', level: 'info' }, { pattern: 'step', limit: 2 }, { pattern: 'completed', jobName: discoveredJobNames[0] || 'ImportCatalog' } ]; for (const combo of testCombinations) { const result = await client.callTool('search_job_logs', combo); assertValidMCPResponse(result); assert.equal(result.isError, false, `Combination ${JSON.stringify(combo)} should succeed`); const searchResults = assertSearchResultsFormat(result, combo.pattern, combo.jobName); if (searchResults.entries.length > 0) { // Validate consistent structure regardless of search parameters for (const entry of searchResults.entries) { assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()), `Consistent format for combo ${JSON.stringify(combo)}: "${entry.substring(0, 50)}..."`); assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(entry), `Consistent timestamp for combo ${JSON.stringify(combo)}: "${entry.substring(0, 100)}..."`); } } } }); }); // === Advanced Multi-Step Scenarios === describe('Advanced Multi-Step Scenarios', () => { test('should support complex search refinement workflows', async () => { // Step 1: Broad search to find available data const broadResult = await client.callTool('search_job_logs', { pattern: 'job', limit: 10 }); assertValidMCPResponse(broadResult); const broadSearch = assertSearchResultsFormat(broadResult, 'job'); if (broadSearch.entries.length > 0) { // Step 2: Extract job name from first result for targeted search const firstEntry = broadSearch.entries[0]; const jobMatch = firstEntry.match(/^\[([^\]]+)\]/); if (jobMatch) { const extractedJobName = jobMatch[1]; // Step 3: Refined search using extracted job name const refinedResult = await client.callTool('search_job_logs', { pattern: 'step', jobName: extractedJobName, limit: 3 }); assertValidMCPResponse(refinedResult); const refinedSearch = assertSearchResultsFormat(refinedResult, 'step', extractedJobName); // Step 4: Validate refinement worked correctly if (refinedSearch.entries.length > 0) { for (const entry of refinedSearch.entries) { assert.ok(entry.includes(`[${extractedJobName}]`), `Refined search should only return entries from "${extractedJobName}"`); assert.ok(entry.toLowerCase().includes('step'), `Refined search should contain "step" pattern`); } } } } }); test('should handle progressive pattern narrowing', async () => { // Progressive narrowing from broad to specific patterns const progressivePatterns = ['job', 'step', 'Executing step']; const results = []; for (const pattern of progressivePatterns) { const result = await client.callTool('search_job_logs', { pattern: pattern, limit: 5 }); assertValidMCPResponse(result); const searchResults = assertSearchResultsFormat(result, pattern); results.push({ pattern, matchCount: searchResults.matchCount }); } // Each pattern should be valid (no errors) assert.equal(results.length, progressivePatterns.length, 'All progressive patterns should execute successfully'); // More specific patterns should not return more results than broader ones if (results[0].matchCount > 0 && results[1].matchCount > 0) { assert.ok(results[1].matchCount <= results[0].matchCount, 'More specific patterns should not exceed broader pattern results'); } }); }); // === Performance and Reliability Monitoring === describe('Performance and Reliability', () => { test('should handle sequential search operations reliably', async () => { const searchOperations = [ { pattern: 'INFO', limit: 3 }, { pattern: 'step', limit: 2 }, { pattern: 'completed', limit: 1 } ]; const results = []; // Execute operations sequentially to test reliability for (const operation of searchOperations) { const result = await client.callTool('search_job_logs', operation); assertValidMCPResponse(result); assert.equal(result.isError, false, `Operation ${JSON.stringify(operation)} should succeed`); results.push({ operation, success: !result.isError, hasContent: result.content && result.content.length > 0 }); } // All operations should succeed const successfulOps = results.filter(r => r.success).length; assert.equal(successfulOps, searchOperations.length, 'All sequential operations should succeed'); // All operations should return content const opsWithContent = results.filter(r => r.hasContent).length; assert.equal(opsWithContent, searchOperations.length, 'All operations should return content'); }); test('should provide consistent results for repeated searches', async () => { const testPattern = 'INFO'; const repeatCount = 3; const results = []; // Perform same search multiple times for (let i = 0; i < repeatCount; i++) { const result = await client.callTool('search_job_logs', { pattern: testPattern, limit: 2 }); assertValidMCPResponse(result); assert.equal(result.isError, false, `Iteration ${i + 1} should succeed`); const text = parseResponseText(result.content[0].text); const matchCountMatch = text.match(/Found (\d+) matches/); const matchCount = matchCountMatch ? parseInt(matchCountMatch[1]) : 0; results.push({ iteration: i + 1, matchCount, hasMatches: matchCount > 0 }); } // Results should be consistent across iterations const uniqueMatchCounts = [...new Set(results.map(r => r.matchCount))]; assert.equal(uniqueMatchCounts.length, 1, `Match counts should be consistent across iterations: ${results.map(r => r.matchCount).join(', ')}`); }); }); // === Comprehensive Error Scenario Testing === describe('Complex Error Scenarios', () => { test('should categorize and handle different error types systematically', async () => { const errorScenarios = [ { name: 'validation_error', params: { pattern: '' }, expectedKeywords: ['pattern', 'non-empty', 'string'] }, { name: 'type_error', params: { pattern: 'INFO', limit: 'invalid' }, expectedKeywords: ['Invalid limit', 'number'] }, { name: 'constraint_error', params: { pattern: 'INFO', limit: -1 }, expectedKeywords: ['Invalid limit', 'Must be between'] } ]; for (const scenario of errorScenarios) { const result = await client.callTool('search_job_logs', scenario.params); assertValidMCPResponse(result); assert.equal(result.isError, true, `Scenario "${scenario.name}" should be an error`); const errorText = result.content[0].text.toLowerCase(); // Check if error contains expected keywords const hasExpectedKeywords = scenario.expectedKeywords.some(keyword => errorText.includes(keyword.toLowerCase()) ); assert.ok(hasExpectedKeywords, `Error for "${scenario.name}" should contain keywords: ${scenario.expectedKeywords.join(', ')}. Got: "${result.content[0].text}"`); } }); test('should handle edge cases with complex validation logic', async () => { const edgeCases = [ { name: 'special_characters', pattern: '[test]', expectSuccess: true }, { name: 'unicode_characters', pattern: 'тест', expectSuccess: true }, { name: 'very_long_pattern', pattern: 'a'.repeat(500), expectSuccess: true }, { name: 'pattern_with_quotes', pattern: '"quoted"', expectSuccess: true } ]; for (const edgeCase of edgeCases) { const result = await client.callTool('search_job_logs', { pattern: edgeCase.pattern, limit: 1 }); assertValidMCPResponse(result); if (edgeCase.expectSuccess) { assert.equal(result.isError, false, `Edge case "${edgeCase.name}" should succeed`); // Should handle gracefully even if no matches found const searchResults = assertSearchResultsFormat(result, edgeCase.pattern); assert.ok(searchResults.matchCount >= 0, `Edge case "${edgeCase.name}" should return valid match count`); } else { assert.equal(result.isError, true, `Edge case "${edgeCase.name}" should fail as expected`); } } }); }); }); ``` -------------------------------------------------------------------------------- /docs/dw_catalog/VariationGroup.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.catalog # Class VariationGroup ## Inheritance Hierarchy - Object - dw.object.PersistentObject - dw.object.ExtensibleObject - dw.catalog.Product - dw.catalog.VariationGroup ## Description Class representing a group of variants within a master product who share a common value for one or more variation attribute values. Variation groups are used to simplify merchandising of products. From a more technical perspective, variation groups are defined by two things: A relation to a master product. A set of variation attributes which have fixed values. A variant of the related master product is considered in the group if and only if it matches on the fixed variation attribute values. Similar to a Variant, a VariationGroup does a fallback to the master product for all attributes (name, description, etc) and relations (recommendations, etc). ## Properties ### allProductLinks **Type:** Collection (Read Only) All product links of the product variation group. If the variation group does not define any product links, but the master product does, the product links of the master are returned. ### brand **Type:** String (Read Only) The brand of the product variation group. If the variation group does not define an own value for 'brand', the value of the master product is returned. ### classificationCategory **Type:** Category (Read Only) The classification category of the product variation group. Please note that the classification category is always inherited from the master and cannot be overridden by the variation group. ### custom **Type:** CustomAttributes (Read Only) The custom attributes of the variation group. Custom attributes are inherited from the master product and can be overridden by the variation group. ### EAN **Type:** String (Read Only) The EAN of the product variation group. If the variation group does not define an own value for 'EAN', the value of the master product is returned. ### image **Type:** MediaFile (Read Only) The image of the product variation group. If the variation group does not define an own value for 'image', the value of the master product is returned. ### longDescription **Type:** MarkupText (Read Only) The long description of the product variation group. If the variation group does not define an own value for 'longDescription', the value of the master product is returned. ### manufacturerName **Type:** String (Read Only) The manufacturer name of the product variation group. If the variation group does not define an own value for 'manufacturerName', the value of the master product is returned. ### manufacturerSKU **Type:** String (Read Only) The manufacturer sku of the product variation group. If the variation group does not define an own value for 'manufacturerSKU', the value of the master product is returned. ### masterProduct **Type:** Product (Read Only) The ProductMaster for this mastered product. ### name **Type:** String (Read Only) The name of the product variation group. If the variation group does not define an own value for 'name', the value of the master product is returned. ### onlineFrom **Type:** Date (Read Only) The onlineFrom date of the product variation group. If the variation group does not define an own value for 'onlineFrom', the value of the master product is returned. ### onlineTo **Type:** Date (Read Only) The onlineTo date of the product variation group. If the variation group does not define an own value for 'onlineTo', the value of the master product is returned. ### optionProduct **Type:** boolean (Read Only) Returns 'true' if the variation group has any options, otherwise 'false'. Method also returns 'true' if the variation group has not any options, but the related master product has options. ### pageDescription **Type:** String (Read Only) The pageDescription of the product variation group. If the variation group does not define an own value for 'pageDescription', the value of the master product is returned. ### pageKeywords **Type:** String (Read Only) The pageKeywords of the product variation group. If the variation group does not define an own value for 'pageKeywords', the value of the master product is returned. ### pageTitle **Type:** String (Read Only) The pageTitle of the product variation group. If the variation group does not define an own value for 'pageTitle', the value of the master product is returned. ### pageURL **Type:** String (Read Only) The pageURL of the product variation group. If the variation group does not define an own value for 'pageURL', the value of the master product is returned. ### productLinks **Type:** Collection (Read Only) All product links of the product variation group for which the target product is assigned to the current site catalog. If the variation group does not define any product links, but the master product does, the product links of the master are returned. ### shortDescription **Type:** MarkupText (Read Only) The short description of the product variation group. If the variation group does not define an own value for 'shortDescription', the value of the master product is returned. ### taxClassID **Type:** String (Read Only) The tax class id of the product variation group. If the variation group does not define an own value for 'taxClassID', the value of the master product is returned. ### template **Type:** String (Read Only) The rendering template name of the product variation group. If the variation group does not define an own value for 'template', the value of the master product is returned. ### thumbnail **Type:** MediaFile (Read Only) The thumbnail image of the product variation group. If the variation group does not define an own value for 'thumbnailImage', the value of the master product is returned. ### unit **Type:** String (Read Only) The sales unit of the product variation group as defined by the master product. If the variation group does not define an own value for 'unit', the value of the master product is returned. ### unitQuantity **Type:** Quantity (Read Only) The unitQuantity of the product variation group as defined by the master product. If the variation group does not define an own value for 'unitQuantity', the value of the master product is returned. ### UPC **Type:** String (Read Only) The UPC of the product variation group. If the variation group does not define an own value for 'UPC', the value of the master product is returned. ## Constructor Summary ## Method Summary ### getAllProductLinks **Signature:** `getAllProductLinks() : Collection` Returns all product links of the product variation group. ### getAllProductLinks **Signature:** `getAllProductLinks(type : Number) : Collection` Returns all product links of the specified type of the product variation group. ### getBrand **Signature:** `getBrand() : String` Returns the brand of the product variation group. ### getClassificationCategory **Signature:** `getClassificationCategory() : Category` Returns the classification category of the product variation group. ### getCustom **Signature:** `getCustom() : CustomAttributes` Returns the custom attributes of the variation group. ### getEAN **Signature:** `getEAN() : String` Returns the EAN of the product variation group. ### getImage **Signature:** `getImage() : MediaFile` Returns the image of the product variation group. ### getLongDescription **Signature:** `getLongDescription() : MarkupText` Returns the long description of the product variation group. ### getManufacturerName **Signature:** `getManufacturerName() : String` Returns the manufacturer name of the product variation group. ### getManufacturerSKU **Signature:** `getManufacturerSKU() : String` Returns the manufacturer sku of the product variation group. ### getMasterProduct **Signature:** `getMasterProduct() : Product` Returns the ProductMaster for this mastered product. ### getName **Signature:** `getName() : String` Returns the name of the product variation group. ### getOnlineFrom **Signature:** `getOnlineFrom() : Date` Returns the onlineFrom date of the product variation group. ### getOnlineTo **Signature:** `getOnlineTo() : Date` Returns the onlineTo date of the product variation group. ### getPageDescription **Signature:** `getPageDescription() : String` Returns the pageDescription of the product variation group. ### getPageKeywords **Signature:** `getPageKeywords() : String` Returns the pageKeywords of the product variation group. ### getPageTitle **Signature:** `getPageTitle() : String` Returns the pageTitle of the product variation group. ### getPageURL **Signature:** `getPageURL() : String` Returns the pageURL of the product variation group. ### getProductLinks **Signature:** `getProductLinks() : Collection` Returns all product links of the product variation group for which the target product is assigned to the current site catalog. ### getProductLinks **Signature:** `getProductLinks(type : Number) : Collection` Returns all product links of the specified type of the product variation group for which the target product is assigned to the current site catalog. ### getRecommendations **Signature:** `getRecommendations(type : Number) : Collection` Retrieve the sorted collection of recommendations of the specified type for this product variation group. ### getShortDescription **Signature:** `getShortDescription() : MarkupText` Returns the short description of the product variation group. ### getTaxClassID **Signature:** `getTaxClassID() : String` Returns the tax class id of the product variation group. ### getTemplate **Signature:** `getTemplate() : String` Returns the rendering template name of the product variation group. ### getThumbnail **Signature:** `getThumbnail() : MediaFile` Returns the thumbnail image of the product variation group. ### getUnit **Signature:** `getUnit() : String` Returns the sales unit of the product variation group as defined by the master product. ### getUnitQuantity **Signature:** `getUnitQuantity() : Quantity` Returns the unitQuantity of the product variation group as defined by the master product. ### getUPC **Signature:** `getUPC() : String` Returns the UPC of the product variation group. ### isOptionProduct **Signature:** `isOptionProduct() : boolean` Returns 'true' if the variation group has any options, otherwise 'false'. ## Method Detail ## Method Details ### getAllProductLinks **Signature:** `getAllProductLinks() : Collection` **Description:** Returns all product links of the product variation group. If the variation group does not define any product links, but the master product does, the product links of the master are returned. **Returns:** All product links of the variation group or master --- ### getAllProductLinks **Signature:** `getAllProductLinks(type : Number) : Collection` **Description:** Returns all product links of the specified type of the product variation group. If the variation group does not define any product links, but the master product does, the product links of the master are returned. **Parameters:** - `type`: Type of the product link **Returns:** Product links of specified type of the variation group or master --- ### getBrand **Signature:** `getBrand() : String` **Description:** Returns the brand of the product variation group. If the variation group does not define an own value for 'brand', the value of the master product is returned. **Returns:** The brand of the variation group or master --- ### getClassificationCategory **Signature:** `getClassificationCategory() : Category` **Description:** Returns the classification category of the product variation group. Please note that the classification category is always inherited from the master and cannot be overridden by the variation group. **Returns:** The classification category as defined for the master product of the variation group --- ### getCustom **Signature:** `getCustom() : CustomAttributes` **Description:** Returns the custom attributes of the variation group. Custom attributes are inherited from the master product and can be overridden by the variation group. **Returns:** the custom attributes of the variation group. --- ### getEAN **Signature:** `getEAN() : String` **Description:** Returns the EAN of the product variation group. If the variation group does not define an own value for 'EAN', the value of the master product is returned. **Returns:** The EAN of the variation group or master --- ### getImage **Signature:** `getImage() : MediaFile` **Description:** Returns the image of the product variation group. If the variation group does not define an own value for 'image', the value of the master product is returned. **Returns:** The image of the variation group or master --- ### getLongDescription **Signature:** `getLongDescription() : MarkupText` **Description:** Returns the long description of the product variation group. If the variation group does not define an own value for 'longDescription', the value of the master product is returned. **Returns:** The long description name of the variation group or master --- ### getManufacturerName **Signature:** `getManufacturerName() : String` **Description:** Returns the manufacturer name of the product variation group. If the variation group does not define an own value for 'manufacturerName', the value of the master product is returned. **Returns:** The manufacturer name of the variation group or master --- ### getManufacturerSKU **Signature:** `getManufacturerSKU() : String` **Description:** Returns the manufacturer sku of the product variation group. If the variation group does not define an own value for 'manufacturerSKU', the value of the master product is returned. **Returns:** The manufacturer sku of the variation group or master --- ### getMasterProduct **Signature:** `getMasterProduct() : Product` **Description:** Returns the ProductMaster for this mastered product. **Returns:** the ProductMaster of this mastered product --- ### getName **Signature:** `getName() : String` **Description:** Returns the name of the product variation group. If the variation group does not define an own value for 'name', the value of the master product is returned. **Returns:** The name of the variation group or master --- ### getOnlineFrom **Signature:** `getOnlineFrom() : Date` **Description:** Returns the onlineFrom date of the product variation group. If the variation group does not define an own value for 'onlineFrom', the value of the master product is returned. **Returns:** The onlineFrom date of the variation group or master --- ### getOnlineTo **Signature:** `getOnlineTo() : Date` **Description:** Returns the onlineTo date of the product variation group. If the variation group does not define an own value for 'onlineTo', the value of the master product is returned. **Returns:** The onlineTo date of the variation group or master --- ### getPageDescription **Signature:** `getPageDescription() : String` **Description:** Returns the pageDescription of the product variation group. If the variation group does not define an own value for 'pageDescription', the value of the master product is returned. **Returns:** The pageDescription of the variation group or master --- ### getPageKeywords **Signature:** `getPageKeywords() : String` **Description:** Returns the pageKeywords of the product variation group. If the variation group does not define an own value for 'pageKeywords', the value of the master product is returned. **Returns:** The pageKeywords of the variation group or master --- ### getPageTitle **Signature:** `getPageTitle() : String` **Description:** Returns the pageTitle of the product variation group. If the variation group does not define an own value for 'pageTitle', the value of the master product is returned. **Returns:** The pageTitle of the variation group or master --- ### getPageURL **Signature:** `getPageURL() : String` **Description:** Returns the pageURL of the product variation group. If the variation group does not define an own value for 'pageURL', the value of the master product is returned. **Returns:** The pageURL of the variation group or master --- ### getProductLinks **Signature:** `getProductLinks() : Collection` **Description:** Returns all product links of the product variation group for which the target product is assigned to the current site catalog. If the variation group does not define any product links, but the master product does, the product links of the master are returned. **Returns:** Product links of the variation group or master --- ### getProductLinks **Signature:** `getProductLinks(type : Number) : Collection` **Description:** Returns all product links of the specified type of the product variation group for which the target product is assigned to the current site catalog. If the variation group does not define any product links of the specified type, but the master product does, the product links of the master are returned. **Parameters:** - `type`: Type of the product link **Returns:** Product links of specified type of the variation group or master --- ### getRecommendations **Signature:** `getRecommendations(type : Number) : Collection` **Description:** Retrieve the sorted collection of recommendations of the specified type for this product variation group. The types (cross-sell, up-sell, etc) are enumerated in the dw.catalog.Recommendation class. Only recommendations which are stored in the current site catalog are returned. Furthermore, a recommendation is only returned if the target of the recommendation is assigned to the current site catalog. If the variation group does not define any recommendations, but the master product does, the recommendations of the master are returned. **Parameters:** - `type`: the recommendation type **Returns:** the sorted collection, never null but possibly empty. --- ### getShortDescription **Signature:** `getShortDescription() : MarkupText` **Description:** Returns the short description of the product variation group. If the variation group does not define an own value for 'shortDescription', the value of the master product is returned. **Returns:** The short description name of the variation group or master --- ### getTaxClassID **Signature:** `getTaxClassID() : String` **Description:** Returns the tax class id of the product variation group. If the variation group does not define an own value for 'taxClassID', the value of the master product is returned. **Returns:** The tax class id of the variation group or master --- ### getTemplate **Signature:** `getTemplate() : String` **Description:** Returns the rendering template name of the product variation group. If the variation group does not define an own value for 'template', the value of the master product is returned. **Returns:** The rendering template name of the variation group or master --- ### getThumbnail **Signature:** `getThumbnail() : MediaFile` **Description:** Returns the thumbnail image of the product variation group. If the variation group does not define an own value for 'thumbnailImage', the value of the master product is returned. **Returns:** The thumbnail image of the variation group or master --- ### getUnit **Signature:** `getUnit() : String` **Description:** Returns the sales unit of the product variation group as defined by the master product. If the variation group does not define an own value for 'unit', the value of the master product is returned. **Returns:** The sales unit of the variation group or master --- ### getUnitQuantity **Signature:** `getUnitQuantity() : Quantity` **Description:** Returns the unitQuantity of the product variation group as defined by the master product. If the variation group does not define an own value for 'unitQuantity', the value of the master product is returned. **Returns:** The unitQuantity of the variation group or master --- ### getUPC **Signature:** `getUPC() : String` **Description:** Returns the UPC of the product variation group. If the variation group does not define an own value for 'UPC', the value of the master product is returned. **Returns:** The UPC of the variation group or master --- ### isOptionProduct **Signature:** `isOptionProduct() : boolean` **Description:** Returns 'true' if the variation group has any options, otherwise 'false'. Method also returns 'true' if the variation group has not any options, but the related master product has options. **Returns:** true if the variation group has any options, false otherwise. --- ```