This is page 35 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?format=md&lines=true&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

--------------------------------------------------------------------------------
/docs/dw_util/StringUtils.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Package: dw.util
  2 | 
  3 | # Class StringUtils
  4 | 
  5 | ## Inheritance Hierarchy
  6 | 
  7 | - Object
  8 |   - dw.util.StringUtils
  9 | 
 10 | ## Description
 11 | 
 12 | String utility class.
 13 | 
 14 | ## Constants
 15 | 
 16 | ### ENCODE_TYPE_HTML
 17 | 
 18 | **Type:** Number = 0
 19 | 
 20 | String encoding type HTML.
 21 | 
 22 | ### ENCODE_TYPE_WML
 23 | 
 24 | **Type:** Number = 2
 25 | 
 26 | String encoding type WML.
 27 | 
 28 | ### ENCODE_TYPE_XML
 29 | 
 30 | **Type:** Number = 1
 31 | 
 32 | String encoding type XML.
 33 | 
 34 | ### TRUNCATE_CHAR
 35 | 
 36 | **Type:** String = "char"
 37 | 
 38 | String truncate mode 'char'. Truncate string to the nearest character. Default mode if no truncate mode is specified.
 39 | 
 40 | ### TRUNCATE_SENTENCE
 41 | 
 42 | **Type:** String = "sentence"
 43 | 
 44 | String truncate mode 'sentence'. Truncate string to the nearest sentence.
 45 | 
 46 | ### TRUNCATE_WORD
 47 | 
 48 | **Type:** String = "word"
 49 | 
 50 | String truncate mode 'word'. Truncate string to the nearest word.
 51 | 
 52 | ## Properties
 53 | 
 54 | ## Constructor Summary
 55 | 
 56 | ## Method Summary
 57 | 
 58 | ### decodeBase64
 59 | 
 60 | **Signature:** `static decodeBase64(base64 : String) : String`
 61 | 
 62 | Interprets a Base64 encoded string as byte stream of an UTF-8 encoded string.
 63 | 
 64 | ### decodeBase64
 65 | 
 66 | **Signature:** `static decodeBase64(base64 : String, characterEncoding : String) : String`
 67 | 
 68 | Interprets a Base64 encoded string as the byte stream representation of a string.
 69 | 
 70 | ### decodeString
 71 | 
 72 | **Signature:** `static decodeString(str : String, type : Number) : String`
 73 | 
 74 | Convert a given syntax-safe string to a string according to the selected character entity encoding type.
 75 | 
 76 | ### encodeBase64
 77 | 
 78 | **Signature:** `static encodeBase64(str : String) : String`
 79 | 
 80 | Encodes the byte representation of the given string as Base64.
 81 | 
 82 | ### encodeBase64
 83 | 
 84 | **Signature:** `static encodeBase64(str : String, characterEncoding : String) : String`
 85 | 
 86 | Encodes the byte representation of the given string as Base64.
 87 | 
 88 | ### encodeString
 89 | 
 90 | **Signature:** `static encodeString(str : String, type : Number) : String`
 91 | 
 92 | Convert a given string to a syntax-safe string according to the selected character entity encoding type.
 93 | 
 94 | ### format
 95 | 
 96 | **Signature:** `static format(format : String, args : Object...) : String`
 97 | 
 98 | Returns a formatted string using the specified format and arguments.
 99 | 
100 | ### formatCalendar
101 | 
102 | **Signature:** `static formatCalendar(calendar : Calendar) : String`
103 | 
104 | 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.
105 | 
106 | ### formatCalendar
107 | 
108 | **Signature:** `static formatCalendar(calendar : Calendar, format : String) : String`
109 | 
110 | Formats a Calendar object with the provided date format.
111 | 
112 | ### formatCalendar
113 | 
114 | **Signature:** `static formatCalendar(calendar : Calendar, locale : String, pattern : Number) : String`
115 | 
116 | Formats a Calendar object with the date format defined by the provided locale and Calendar pattern.
117 | 
118 | ### formatDate
119 | 
120 | **Signature:** `static formatDate(date : Date) : String`
121 | 
122 | Formats a date with the default date format of the current site.
123 | 
124 | ### formatDate
125 | 
126 | **Signature:** `static formatDate(date : Date, format : String) : String`
127 | 
128 | Formats a date with the provided date format.
129 | 
130 | ### formatDate
131 | 
132 | **Signature:** `static formatDate(date : Date, format : String, locale : String) : String`
133 | 
134 | Formats a date with the provided date format in specified locale.
135 | 
136 | ### formatInteger
137 | 
138 | **Signature:** `static formatInteger(number : Number) : String`
139 | 
140 | Returns a formatted integer number using the default integer format of the current site.
141 | 
142 | ### formatMoney
143 | 
144 | **Signature:** `static formatMoney(money : Money) : String`
145 | 
146 | Formats a Money Object with the default money format of the current request locale.
147 | 
148 | ### formatNumber
149 | 
150 | **Signature:** `static formatNumber(number : Number) : String`
151 | 
152 | Returns a formatted number using the default number format of the current site.
153 | 
154 | ### formatNumber
155 | 
156 | **Signature:** `static formatNumber(number : Number, format : String) : String`
157 | 
158 | Returns a formatted string using the specified number and format.
159 | 
160 | ### formatNumber
161 | 
162 | **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String`
163 | 
164 | Returns a formatted number as a string using the specified number format in specified locale.
165 | 
166 | ### formatNumber
167 | 
168 | **Signature:** `static formatNumber(number : Number, format : String) : String`
169 | 
170 | Returns a formatted string using the specified number and format.
171 | 
172 | ### formatNumber
173 | 
174 | **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String`
175 | 
176 | Returns a formatted number as a string using the specified number format in specified locale.
177 | 
178 | ### garble
179 | 
180 | **Signature:** `static garble(str : String, replaceChar : String, suffixLength : Number) : String`
181 | 
182 | 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.
183 | 
184 | ### ltrim
185 | 
186 | **Signature:** `static ltrim(str : String) : String`
187 | 
188 | Returns the string with leading white space removed.
189 | 
190 | ### pad
191 | 
192 | **Signature:** `static pad(str : String, width : Number) : String`
193 | 
194 | This method provides cell padding functionality to the template.
195 | 
196 | ### rtrim
197 | 
198 | **Signature:** `static rtrim(str : String) : String`
199 | 
200 | Returns the string with trailing white space removed.
201 | 
202 | ### stringToHtml
203 | 
204 | **Signature:** `static stringToHtml(str : String) : String`
205 | 
206 | Convert a given string to an HTML-safe string.
207 | 
208 | ### stringToWml
209 | 
210 | **Signature:** `static stringToWml(str : String) : String`
211 | 
212 | Converts a given string to a WML-safe string.
213 | 
214 | ### stringToXml
215 | 
216 | **Signature:** `static stringToXml(str : String) : String`
217 | 
218 | Converts a given string to a XML-safe string.
219 | 
220 | ### trim
221 | 
222 | **Signature:** `static trim(str : String) : String`
223 | 
224 | Returns the string with leading and trailing white space removed.
225 | 
226 | ### truncate
227 | 
228 | **Signature:** `static truncate(str : String, maxLength : Number, mode : String, suffix : String) : String`
229 | 
230 | Truncate the string to the specified length using specified truncate mode.
231 | 
232 | ## Method Detail
233 | 
234 | ## Method Details
235 | 
236 | ### decodeBase64
237 | 
238 | **Signature:** `static decodeBase64(base64 : String) : String`
239 | 
240 | **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.
241 | 
242 | **Parameters:**
243 | 
244 | - `base64`: the Base64 encoded string - should not be empty or null.
245 | 
246 | **Returns:**
247 | 
248 | the decoded string.
249 | 
250 | ---
251 | 
252 | ### decodeBase64
253 | 
254 | **Signature:** `static decodeBase64(base64 : String, characterEncoding : String) : String`
255 | 
256 | **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.
257 | 
258 | **Parameters:**
259 | 
260 | - `base64`: the Base64 encoded string - should not be empty or null.
261 | - `characterEncoding`: the character encoding to read the input string - should not be empty or null.
262 | 
263 | **Returns:**
264 | 
265 | the decoded string.
266 | 
267 | ---
268 | 
269 | ### decodeString
270 | 
271 | **Signature:** `static decodeString(str : String, type : Number) : String`
272 | 
273 | **Description:** Convert a given syntax-safe string to a string according to the selected character entity encoding type.
274 | 
275 | **Parameters:**
276 | 
277 | - `str`: String to be decoded
278 | - `type`: decode type
279 | 
280 | **Returns:**
281 | 
282 | decoded string
283 | 
284 | ---
285 | 
286 | ### encodeBase64
287 | 
288 | **Signature:** `static encodeBase64(str : String) : String`
289 | 
290 | **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.
291 | 
292 | **Parameters:**
293 | 
294 | - `str`: the string to encode - should not be empty or null.
295 | 
296 | **Returns:**
297 | 
298 | the encoded string.
299 | 
300 | ---
301 | 
302 | ### encodeBase64
303 | 
304 | **Signature:** `static encodeBase64(str : String, characterEncoding : String) : String`
305 | 
306 | **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.
307 | 
308 | **Parameters:**
309 | 
310 | - `str`: the string to encode - should not be empty or null.
311 | - `characterEncoding`: the character encoding to read the input string - should not be empty or null.
312 | 
313 | **Returns:**
314 | 
315 | the encoded string.
316 | 
317 | ---
318 | 
319 | ### encodeString
320 | 
321 | **Signature:** `static encodeString(str : String, type : Number) : String`
322 | 
323 | **Description:** Convert a given string to a syntax-safe string according to the selected character entity encoding type.
324 | 
325 | **Parameters:**
326 | 
327 | - `str`: String to be encoded
328 | - `type`: encode type
329 | 
330 | **Returns:**
331 | 
332 | encoded string
333 | 
334 | ---
335 | 
336 | ### format
337 | 
338 | **Signature:** `static format(format : String, args : Object...) : String`
339 | 
340 | **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.
341 | 
342 | **Parameters:**
343 | 
344 | - `format`: Java like formatting string.
345 | - `args`: optional list of arguments or a collection, which are included into the result string
346 | 
347 | **Returns:**
348 | 
349 | the formatted result string.
350 | 
351 | ---
352 | 
353 | ### formatCalendar
354 | 
355 | **Signature:** `static formatCalendar(calendar : Calendar) : String`
356 | 
357 | **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.
358 | 
359 | **Parameters:**
360 | 
361 | - `calendar`: the calendar object.
362 | 
363 | **Returns:**
364 | 
365 | a string representation of the formatted calendar object.
366 | 
367 | ---
368 | 
369 | ### formatCalendar
370 | 
371 | **Signature:** `static formatCalendar(calendar : Calendar, format : String) : String`
372 | 
373 | **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.
374 | 
375 | **Parameters:**
376 | 
377 | - `calendar`: the calendar object to be printed
378 | - `format`: the format to use.
379 | 
380 | **Returns:**
381 | 
382 | a string representation of the formatted calendar object.
383 | 
384 | ---
385 | 
386 | ### formatCalendar
387 | 
388 | **Signature:** `static formatCalendar(calendar : Calendar, locale : String, pattern : Number) : String`
389 | 
390 | **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.
391 | 
392 | **Parameters:**
393 | 
394 | - `calendar`: the calendar object to be printed
395 | - `locale`: the locale, which defines the date format to be used
396 | - `pattern`: the pattern is one of a calendar pattern e.g. SHORT_DATE_PATTERN as defined in the regional settings for the locale
397 | 
398 | **Returns:**
399 | 
400 | a string representation of the formatted calendar object.
401 | 
402 | ---
403 | 
404 | ### formatDate
405 | 
406 | **Signature:** `static formatDate(date : Date) : String`
407 | 
408 | **Description:** Formats a date with the default date format of the current site.
409 | 
410 | **Deprecated:**
411 | 
412 | Use formatCalendar(Calendar, String) instead.
413 | 
414 | **Parameters:**
415 | 
416 | - `date`: the date to format.
417 | 
418 | **Returns:**
419 | 
420 | a string representation of the formatted date.
421 | 
422 | ---
423 | 
424 | ### formatDate
425 | 
426 | **Signature:** `static formatDate(date : Date, format : String) : String`
427 | 
428 | **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.
429 | 
430 | **Deprecated:**
431 | 
432 | Use formatCalendar(Calendar, String) instead.
433 | 
434 | **Parameters:**
435 | 
436 | - `date`: the date to format.
437 | - `format`: the format to use.
438 | 
439 | **Returns:**
440 | 
441 | a string representation of the formatted date.
442 | 
443 | ---
444 | 
445 | ### formatDate
446 | 
447 | **Signature:** `static formatDate(date : Date, format : String, locale : String) : String`
448 | 
449 | **Description:** Formats a date with the provided date format in specified locale. The format is Java date format, like "yyyy-MM-DD".
450 | 
451 | **Deprecated:**
452 | 
453 | Use formatCalendar(Calendar, String) instead.
454 | 
455 | **Parameters:**
456 | 
457 | - `date`: the date to format.
458 | - `format`: the format to use.
459 | - `locale`: the locale to use.
460 | 
461 | **Returns:**
462 | 
463 | a string representation of the formatted date.
464 | 
465 | ---
466 | 
467 | ### formatInteger
468 | 
469 | **Signature:** `static formatInteger(number : Number) : String`
470 | 
471 | **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.
472 | 
473 | **Parameters:**
474 | 
475 | - `number`: the number to format.
476 | 
477 | **Returns:**
478 | 
479 | a formatted an integer number with the default integer format of the current site.
480 | 
481 | ---
482 | 
483 | ### formatMoney
484 | 
485 | **Signature:** `static formatMoney(money : Money) : String`
486 | 
487 | **Description:** Formats a Money Object with the default money format of the current request locale.
488 | 
489 | **Parameters:**
490 | 
491 | - `money`: The Money instance that should be formatted.
492 | 
493 | **Returns:**
494 | 
495 | The formatted String representation of the passed money. In case of an error the string 'N/A' is returned.
496 | 
497 | ---
498 | 
499 | ### formatNumber
500 | 
501 | **Signature:** `static formatNumber(number : Number) : String`
502 | 
503 | **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.
504 | 
505 | **Parameters:**
506 | 
507 | - `number`: the number to format.
508 | 
509 | **Returns:**
510 | 
511 | a formatted number using the default number format of the current site.
512 | 
513 | ---
514 | 
515 | ### formatNumber
516 | 
517 | **Signature:** `static formatNumber(number : Number, format : String) : String`
518 | 
519 | **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.
520 | 
521 | **API Versioned:**
522 | 
523 | No longer available as of version 18.10.
524 | 
525 | **Parameters:**
526 | 
527 | - `number`: the number to format.
528 | - `format`: the format to use.
529 | 
530 | **Returns:**
531 | 
532 | a formatted string using the specified number and format.
533 | 
534 | ---
535 | 
536 | ### formatNumber
537 | 
538 | **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String`
539 | 
540 | **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.
541 | 
542 | **API Versioned:**
543 | 
544 | No longer available as of version 18.10.
545 | 
546 | **Parameters:**
547 | 
548 | - `number`: the number to format.
549 | - `format`: the format to use.
550 | - `locale`: the locale to use.
551 | 
552 | **Returns:**
553 | 
554 | a formatted number as a string using the specified number format in specified locale.
555 | 
556 | ---
557 | 
558 | ### formatNumber
559 | 
560 | **Signature:** `static formatNumber(number : Number, format : String) : String`
561 | 
562 | **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).
563 | 
564 | **API Versioned:**
565 | 
566 | From version 18.10. In prior versions this method did fall back to Java formatting rules, instead of using the definitions in regional settings.
567 | 
568 | **Parameters:**
569 | 
570 | - `number`: the number to format.
571 | - `format`: the format to use.
572 | 
573 | **Returns:**
574 | 
575 | a formatted string using the specified number and format.
576 | 
577 | ---
578 | 
579 | ### formatNumber
580 | 
581 | **Signature:** `static formatNumber(number : Number, format : String, locale : String) : String`
582 | 
583 | **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).
584 | 
585 | **API Versioned:**
586 | 
587 | From version 18.10. In prior versions this method did fall back to Java formatting rules, instead of using the definitions in regional settings.
588 | 
589 | **Parameters:**
590 | 
591 | - `number`: the number to format.
592 | - `format`: the format to use.
593 | - `locale`: the locale to use.
594 | 
595 | **Returns:**
596 | 
597 | a formatted number as a string using the specified number format in specified locale.
598 | 
599 | ---
600 | 
601 | ### garble
602 | 
603 | **Signature:** `static garble(str : String, replaceChar : String, suffixLength : Number) : String`
604 | 
605 | **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.
606 | 
607 | **Parameters:**
608 | 
609 | - `str`: String to garble
610 | - `replaceChar`: character to use as a replacement
611 | - `suffixLength`: length of the suffix
612 | 
613 | **Returns:**
614 | 
615 | the garbled string.
616 | 
617 | ---
618 | 
619 | ### ltrim
620 | 
621 | **Signature:** `static ltrim(str : String) : String`
622 | 
623 | **Description:** Returns the string with leading white space removed.
624 | 
625 | **Parameters:**
626 | 
627 | - `str`: the String to remove characters from.
628 | 
629 | **Returns:**
630 | 
631 | the string with leading white space removed.
632 | 
633 | ---
634 | 
635 | ### pad
636 | 
637 | **Signature:** `static pad(str : String, width : Number) : String`
638 | 
639 | **Description:** This method provides cell padding functionality to the template.
640 | 
641 | **Parameters:**
642 | 
643 | - `str`: the string to process
644 | - `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.
645 | 
646 | **Returns:**
647 | 
648 | the processed string.
649 | 
650 | ---
651 | 
652 | ### rtrim
653 | 
654 | **Signature:** `static rtrim(str : String) : String`
655 | 
656 | **Description:** Returns the string with trailing white space removed.
657 | 
658 | **Parameters:**
659 | 
660 | - `str`: the String to remove characters from.
661 | 
662 | **Returns:**
663 | 
664 | the string with trailing white space removed.
665 | 
666 | ---
667 | 
668 | ### stringToHtml
669 | 
670 | **Signature:** `static stringToHtml(str : String) : String`
671 | 
672 | **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.
673 | 
674 | **Parameters:**
675 | 
676 | - `str`: String to be converted.
677 | 
678 | **Returns:**
679 | 
680 | converted string.
681 | 
682 | ---
683 | 
684 | ### stringToWml
685 | 
686 | **Signature:** `static stringToWml(str : String) : String`
687 | 
688 | **Description:** Converts a given string to a WML-safe string. This method substitutes characters that conflict with WML syntax (<,>,&,',"$) to WML named character entities.
689 | 
690 | **Deprecated:**
691 | 
692 | Don't use this method anymore
693 | 
694 | **Parameters:**
695 | 
696 | - `str`: String to be converted.
697 | 
698 | **Returns:**
699 | 
700 | the converted string.
701 | 
702 | ---
703 | 
704 | ### stringToXml
705 | 
706 | **Signature:** `static stringToXml(str : String) : String`
707 | 
708 | **Description:** Converts a given string to a XML-safe string. This method substitutes characters that conflict with XML syntax (<,>,&,',") to XML named character entities.
709 | 
710 | **Parameters:**
711 | 
712 | - `str`: String to be converted.
713 | 
714 | **Returns:**
715 | 
716 | the converted string.
717 | 
718 | ---
719 | 
720 | ### trim
721 | 
722 | **Signature:** `static trim(str : String) : String`
723 | 
724 | **Description:** Returns the string with leading and trailing white space removed.
725 | 
726 | **Parameters:**
727 | 
728 | - `str`: the string to trim.
729 | 
730 | **Returns:**
731 | 
732 | the string with leading and trailing white space removed.
733 | 
734 | ---
735 | 
736 | ### truncate
737 | 
738 | **Signature:** `static truncate(str : String, maxLength : Number, mode : String, suffix : String) : String`
739 | 
740 | **Description:** Truncate the string to the specified length using specified truncate mode. Optionally, append suffix to truncated string.
741 | 
742 | **Parameters:**
743 | 
744 | - `str`: string to truncate
745 | - `maxLength`: maximum length of the truncated string, not including suffix
746 | - `mode`: truncate mode (TRUNCATE_CHAR, TRUNCATE_WORD, TRUNCATE_SENTENCE), if null TRUNCATE_CHAR is assumed
747 | - `suffix`: suffix append to the truncated string
748 | 
749 | **Returns:**
750 | 
751 | the truncated string.
752 | 
753 | ---
```

--------------------------------------------------------------------------------
/docs/best-practices/performance.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Salesforce B2C Commerce Cloud: Performance Best Practices
  2 | 
  3 | This document outlines key performance optimization strategies for Salesforce B2C Commerce Cloud, focusing on caching and efficient data retrieval.
  4 | 
  5 | ---
  6 | 
  7 | ## Performance and Stability Coding Standards
  8 | 
  9 | 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.
 10 | 
 11 | ### Data Transfer Volume
 12 | 
 13 | B2C Commerce imposes limits on incoming and outgoing network traffic for B2C Commerce instances and the Content Delivery Network.
 14 | 
 15 | 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.
 16 | 
 17 | ### Storefront Development for Performance and Stability
 18 | 
 19 | When developing your storefront, consider storefront development best practices:
 20 | 
 21 | #### Search and Product Processing
 22 | 
 23 | - **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.
 24 | 
 25 | - **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:
 26 |   - Use pipelet Search with input parameter `OrderableProductsOnly` to deal with variation product availability
 27 |   - Use `dw.catalog.ProductSearchHit.getRepresentedVariationValues()` to determine available variation values  
 28 |   - Use `dw.catalog.ProductSearchHit.minPrice` or `Product.priceMode.minPrice` to determine price ranges
 29 | 
 30 | - **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.
 31 | 
 32 | #### External System Integration
 33 | 
 34 | - **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.
 35 | 
 36 | #### Long-Running Operations
 37 | 
 38 | - **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.
 39 | 
 40 | #### Concurrency and Data Integrity
 41 | 
 42 | - **Avoid concurrent changes to the same object.** Storefront controllers and pipelines should only:
 43 |   - Read shared data (for example, catalogs and prices)
 44 |   - Read or write customer-specific data (for example, customer profiles, shopping carts or orders)
 45 | 
 46 | - 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).
 47 | 
 48 | - 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.
 49 | 
 50 | - 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.
 51 | 
 52 | #### Transaction Management
 53 | 
 54 | - **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.
 55 | 
 56 | #### Critical Page Performance
 57 | 
 58 | - **Make sure that the most visited pages are cacheable and well performing.** These controllers and pages are usually:
 59 |   - Category page or search result pages (Search-Show)
 60 |   - Product detail pages (Product-Show)  
 61 |   - Home pages (Default-Start, Home-Show)
 62 |   - Cart Page (Cart-Show)
 63 |   - Checkout pages
 64 | 
 65 | - **Limit expensive (> 10 ms) custom server logic** on OnSession and OnRequest controllers.
 66 | 
 67 | ### Use Index-Friendly APIs
 68 | 
 69 | Replace database intensive or inefficient APIs with appropriate index-friendly APIs. Check code for database intensive APIs in most-visited pages:
 70 | 
 71 | #### Avoid These Database-Intensive APIs:
 72 | - `Category.getProducts()`
 73 | - `Category.getOnlineProducts()`  
 74 | - `Category.getProductAssignments()`
 75 | - `Category.getOnlineCategoryAssignments()`
 76 | - `ProductMgr.queryAllSiteProducts()`
 77 | - `Product.getPriceModel()`
 78 | - `Product.getVariants()`
 79 | - `Product.getVariationModel()`
 80 | 
 81 | #### Use These Index-Friendly APIs Instead:
 82 | - `ProductSearchModel.search()`
 83 | - `ProductSearchModel.orderableProductsOnly(true)`
 84 | - `ProductSearchModel.getRefinements()`
 85 | - `ProductSearchRefinements.getNextLevelRefinementValues()`
 86 | - `ProductSearchModel.getProductSearchHits()`
 87 | - `ProductSearchHit.getMinPrice()`
 88 | - `ProductSearchHit.getMaxPrice()`
 89 | - `ProductSearchHit.getRepresentedProductIDs()`
 90 | - `ProductSearchHit.getRepresentedVariationValues(attribute)`
 91 | 
 92 | ### Additional Performance Requirements
 93 | 
 94 | - **Ensure all direct 3rd-party HTTP calls are migrated to Web Service Framework**
 95 | - **Ensure no Enforced quota violations** are reported in STAGING and PRODUCTION, and that Quota Dashboard alerts have been subscribed by all site admins
 96 | - **Ensure there isn't unnecessary creation** of custom Session objects, productlist objects, or cookies
 97 | - **Ensure a WishList isn't created for every anonymous user** (e.g., created at the end of every product item add to cart calls)
 98 | - **Ensure Custom Object volume is kept in check** with purge jobs
 99 | 
100 | #### OCAPI Specific Requirements:
101 | - **Ensure Shop API GET requests are limited to smaller blocks of data.** Instead of 200 products payload, retrieve 100 or 50
102 | - **Ensure there's no OCAPI request of persistent objects within a hook customization** such as `ProductMgr.getProduct()` or `product.getVariations()`
103 | 
104 | #### SFRA Specific Requirements:
105 | - **Ensure SFRA templates don't include multi-part, embedded, or nested forms.** We don't recommend them as a best practice
106 | - **Ensure that controllers don't call each other,** because controller functionality should be self-contained to avoid circular dependencies
107 | - **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
108 | 
109 | ### Job Development for Performance and Stability
110 | 
111 | To optimize job performance, follow the job development standards:
112 | 
113 | #### Import and Data Processing
114 | 
115 | - **To modify objects in Salesforce B2C Commerce, use standard imports instead of customizations.** In jobs, use B2C Commerce Job Steps for imports.
116 | 
117 | - **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.
118 | 
119 | - **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.
120 | 
121 | #### Data Quality and Validation
122 | 
123 | - **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.
124 | 
125 | #### Memory Management
126 | 
127 | When processing large data sets, pay attention to the memory footprint:
128 | 
129 | - **Design loop logic so that memory consumption doesn't increase with result set size**
130 | - **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
131 | - **Stream data to file regularly** (do not build large structures in memory)
132 | - **Read feeds record by record** (do not read an entire file into memory)
133 | - **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
134 | 
135 | #### Concurrency and Resource Management
136 | 
137 | - **Avoid concurrent changes to the same object.** Use the locking framework to ensure exclusive access. Specify named resources for job schedules.
138 | 
139 | - **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.
140 | 
141 | #### Recovery and Reliability
142 | 
143 | - **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.
144 | 
145 | - **Don't start many jobs at the same time.** Instead, disperse job start times to balance the job load.
146 | 
147 | ---
148 | 
149 | ## 1. Page Caching
150 | 
151 | 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.
152 | 
153 | ### Controller-Driven Caching (Best Practice)
154 | 
155 | Control caching within your controller using the response object. This is superior to the legacy `<iscache>` tag.
156 | 
157 | `response.setExpires(milliseconds)`: Sets a cache duration for the entire page response.
158 | 
159 | **Example:**
160 | 
161 | ```javascript
162 | // cartridge/controllers/Product.js
163 | var server = require('server');
164 | 
165 | server.get('Show', function (req, res, next) {
166 |     // Cache for 24 hours
167 |     var oneDay = 24 * 60 * 60 * 1000;
168 |     response.setExpires(Date.now() + oneDay);
169 | 
170 |     res.render('product/productDetails');
171 |     next();
172 | });
173 | ```
174 | 
175 | ### Remote Includes for Dynamic Content
176 | 
177 | 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]
178 | 
179 | **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]
180 | 
181 | ### Cache Key Strategy
182 | 
183 | The cache key is the full URL. To maximize the cache hit ratio:
184 | 
185 | - **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]
186 | - **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]
187 | 
188 | ---
189 | 
190 | ## 2. Custom Caches (`CacheMgr`)
191 | 
192 | Use custom caches for application-level data caching within dynamic requests (e.g., cart, checkout) where page caching isn't possible. 
193 | 
194 | **Use Cases:**
195 | - Caching expensive calculations (e.g., iterating variants to check for a sale). 
196 | - Caching responses from external services (e.g., inventory, ratings).
197 | 
198 | ### Implementation
199 | 
200 | 1. **Define in `caches.json`:**
201 |    ```json
202 |    {
203 |        "caches": [
204 |            {
205 |                "name": "ExternalAPICache",
206 |                "ttl": 300
207 |            }
208 |        ]
209 |    }
210 |    ```
211 | 
212 | 2. **Register in `package.json`:**
213 |    ```json
214 |    {
215 |      "caches": "./cartridge/scripts/caches.json"
216 |    }
217 |    ```
218 | 
219 | ### The "Get-or-Load" Pattern (Required)
220 | 
221 | Always use the atomic `cache.get(key, loader)` method to prevent a "thundering herd" problem on cache misses. [6, 9]
222 | 
223 | **Example:**
224 | 
225 | ```javascript
226 | var CacheMgr = require('dw/system/CacheMgr');
227 | var MyHTTPService = require('~/cartridge/scripts/services/myHTTPService');
228 | 
229 | function getExternalData() {
230 |     var apiCache = CacheMgr.getCache('ExternalAPICache');
231 |     var cacheKey = 'myExternalData';
232 | 
233 |     // get() executes the loader function ONLY on a cache miss.
234 |     var data = apiCache.get(cacheKey, function () {
235 |         // This expensive call only runs if data is not in cache.
236 |         var result = MyHTTPService.getService().call();
237 |         return result.ok ? JSON.parse(result.object.text) : null;
238 |     });
239 | 
240 |     return data;
241 | }
242 | ```
243 | 
244 | **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.
245 | 
246 | ## 3. ProductSearchModel vs. ProductMgr
247 | 
248 | This is a critical performance distinction.
249 | 
250 | - **ProductSearchModel (PSM):** Queries the fast, optimized Search Index. Use for any list of products (PLPs, search results, filtering).
251 | - **ProductMgr:** Queries the live Database. Use only to get a single, known product by its ID (e.g., on a PDP).
252 | 
253 | ### The N+1 Anti-Pattern (CRITICAL)
254 | 
255 | **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.
256 | 
257 | **Incorrect (Anti-Pattern):**
258 | 
259 | ```javascript
260 | // In a PLP template, looping over search results
261 | var psm = new ProductSearchModel();
262 | //... configure psm...
263 | psm.search();
264 | var hits = psm.getProductSearchHits();
265 | while (hits.hasNext()) {
266 |     var hit = hits.next();
267 |     // ANTI-PATTERN: Calling ProductMgr in a loop!
268 |     var product = ProductMgr.getProduct(hit.getProductID()); 
269 |     //... do something with the full product object...
270 | }
271 | ```
272 | 
273 | **Correct:**
274 | 
275 | 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.
276 | 
277 | ```javascript
278 | // In a PLP template, looping over search results
279 | var psm = new ProductSearchModel();
280 | //... configure psm...
281 | psm.search();
282 | var hits = psm.getProductSearchHits();
283 | while (hits.hasNext()) {
284 |     var hit = hits.next();
285 |     // CORRECT: Use the hit object directly for name, price, etc.
286 |     var minPrice = hit.getMinPrice();
287 |     var variationValues = hit.getRepresentedVariationValues('color');
288 |     //... render tile using data from 'hit'...
289 | }
290 | ```
291 | 
292 | ## 4. Caching in OCAPI/SCAPI Hooks
293 | 
294 | The caching models for OCAPI and SCAPI are fundamentally different.
295 | 
296 | - **OCAPI:** The hook runs before the response is cached. The modified response is what gets stored in OCAPI's application-tier cache.
297 | - **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.
298 | 
299 | ### SCAPI Web-Tier Cache Fundamentals
300 | 
301 | 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.
302 | 
303 | ### Personalized Cache Keys
304 | 
305 | When personalization is enabled for a SCAPI resource, the cache key includes the following in addition to the URL string:
306 | 
307 | - Active promotions
308 | - Active product sorting rules
309 | - Applicable price books
310 | - Active AB test groups
311 | 
312 | 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.
313 | 
314 | 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.
315 | 
316 | ### Script-Level Cache Controls
317 | 
318 | Use the Script API to adjust cache policies dynamically:
319 | 
320 | - `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).
321 | - `dw.system.Response#setVaryBy('price_promotion')`: Opts into personalized caching for price- or promotion-sensitive responses.
322 | 
323 | ```javascript
324 | exports.modifyGETResponse = function (scriptCategory, categoryWO) {
325 |     // Cache for one hour instead of the default TTL
326 |     response.setExpires(Date.now() + 3_600_000);
327 | 
328 |     // Optional: personalize by price & promotion eligibility
329 |     response.setVaryBy('price_promotion');
330 | 
331 |     return new Status(Status.OK);
332 | };
333 | ```
334 | 
335 | ### Best Practices
336 | 
337 | - **OCAPI Hook:** Your modifications will be cached. Keep the logic simple and avoid slow calls like `ProductMgr.getProduct()`.
338 | - **SCAPI Hook:** To cache a modification, you must create a unique cache key by adding a custom query parameter to the URL.
339 | 
340 | **Example (SCAPI):**
341 | 
342 | ```javascript
343 | // Client makes a call with a custom parameter
344 | // GET /shopper-products/v1/.../products/my-prod?c_view=light
345 | 
346 | // SCAPI Hook Script (product.js)
347 | exports.modifyResponse = function (product, productResponse) {
348 |     // Logic is conditional on the custom parameter
349 |     if (request.httpParameters.c_view === 'light') {
350 |         delete productResponse.long_description;
351 |     }
352 | };
353 | ```
354 | 
355 | This creates a separate, cacheable version of the response for the `c_view=light` URL.
356 | 
357 | **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).
358 | 
359 | ## 5. Caching in Custom SCAPI Endpoints
360 | 
361 | You can build your own REST endpoints that integrate with SCAPI's caching, security, and other framework features.
362 | 
363 | ### Implementation
364 | 
365 | Custom endpoints can leverage the same powerful web-tier page cache. Enable it by calling `response.setExpires()` in your implementation script.
366 | 
367 | **Example:**
368 | 
369 | ```javascript
370 | // cartridge/rest-apis/my-api/v1/script.js
371 | var RESTResponseMgr = require('dw/system/RESTResponseMgr');
372 | 
373 | exports.getLoyaltyInfo = function (params) {
374 |     var loyaltyData = { id: params.c_customer_id, points: 1234 };
375 | 
376 |     // Cache this custom API response for 5 minutes
377 |     response.setExpires(Date.now() + (5 * 60 * 1000));
378 | 
379 |     RESTResponseMgr.createSuccess(loyaltyData).render();
380 | };
381 | exports.getLoyaltyInfo.public = true;
382 | ```
383 | 
384 | ### Two-Tier Caching Pattern (for External Services)
385 | 
386 | For maximum resilience and performance when calling external systems, combine both cache layers:
387 | 
388 | 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.
389 | 
390 | 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()`.
391 | 
392 | 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.
393 | 
```

--------------------------------------------------------------------------------
/docs/dw_order/BasketMgr.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Package: dw.order
  2 | 
  3 | # Class BasketMgr
  4 | 
  5 | ## Inheritance Hierarchy
  6 | 
  7 | - Object
  8 |   - dw.order.BasketMgr
  9 | 
 10 | ## Description
 11 | 
 12 | Provides static helper methods for managing baskets.
 13 | 
 14 | ## Properties
 15 | 
 16 | ### baskets
 17 | 
 18 | **Type:** List (Read Only)
 19 | 
 20 | Retrieve all open baskets for the logged in customer including the temporary baskets.
 21 |  
 22 |  
 23 |  Restricted to agent scenario use cases: The returned list contains all agent baskets created with
 24 |  createAgentBasket() and the current storefront basket which can also be retrieved with
 25 |  getCurrentBasket(). This method will result in an exception if called by a user without permission
 26 |  Create_Order_On_Behalf_Of or if no customer is logged in the session.
 27 |  
 28 |  
 29 |  Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore.
 30 | 
 31 | ### currentBasket
 32 | 
 33 | **Type:** Basket (Read Only)
 34 | 
 35 | This method returns the current valid basket of the session customer or null if no current valid
 36 |  basket exists.
 37 |  
 38 |  The methods getCurrentBasket() and getCurrentOrNewBasket() work based on the selected basket
 39 |  persistence, which can be configured in the Business Manager site preferences / baskets section. A basket is
 40 |  valid for the configured basket lifetime.
 41 |  
 42 |  
 43 |  In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also
 44 |  utilizing PWA Kit or other custom headless solution for another part of the same site), this method must
 45 |  NOT be used. Instead, retrieve baskets via GET baskets/{basketId} or
 46 |  GET customers/{customerId}/baskets. Do not use getCurrentOrNewBasket() for basket creation
 47 |  in any scenario.
 48 |  
 49 |  
 50 |  The current basket, if one exists, is usually updated by the method. In particular the last-modified date is
 51 |  updated. No update is done when method getCurrentBasket() is used within a read-only hook
 52 |  implementation (such as a beforeGet or a modifyResponse hook). The lifetime of a basket can be extended
 53 |  in 2 ways:
 54 |  
 55 |  The basket is modified in some way, e.g. a product is added resulting in the basket total being newly
 56 |  calculated. This results in the basket lifetime being reset.
 57 |  The basket has not been modified for 60 minutes, then using this method to access the basket will also reset
 58 |  the basket lifetime.
 59 |  
 60 |  
 61 |  
 62 |  What happens when a customer logs in? Personal data held inside the basket such as addresses, email addresses and
 63 |  payment settings is associated with the customer to whom the basket belongs. If the basket being updated belongs
 64 |  to a different customer this data is removed. This happens when a guest customer that has a basket logs in and
 65 |  hence identifies as a registered customer. In this case the basket which was previously created by the guest
 66 |  customer gets transferred to the (now logged in) registered customer. Should the registered customer already have
 67 |  a basket, this basket is effectively invalidated, but made available using getStoredBasket() allowing
 68 |  the script to merge content from it if desired.
 69 |  
 70 |  
 71 |  What happens when a customer logs out or when the customer session times out? After the customer logs out, a
 72 |  basket belonging to the registered customer (now logged out) is stored (where applicable) and this method 
 73 |  returns null. Personal data is also cleared when the session times out for a guest customer.
 74 |  
 75 |  
 76 |  The following personal data is cleared:
 77 |  
 78 |  product line items that were added from a wish list
 79 |  shipping method
 80 |  coupon line items
 81 |  gift certificate line items
 82 |  billing and shipping addresses
 83 |  payment instruments
 84 |  buyer email
 85 |  
 86 |  If the session currency no longer matches the basket currency, the basket currency should be updated with
 87 |  Basket.updateCurrency().
 88 |  
 89 |  
 90 |  Typical usage:
 91 | 
 92 |   var basket : Basket = BasketMgr.getCurrentBasket();
 93 |  if (basket) {
 94 |      // do something with basket
 95 |  }
 96 |  
 97 |  
 98 |  
 99 |  Constraints:
100 |  
101 |  The method only accesses the basket for the session customer, an exception is thrown when the session
102 |  customer is null.
103 |  Method getCurrentOrNewBasket() only creates a basket when method getCurrentBasket() returns
104 |  null.
105 | 
106 | ### currentOrNewBasket
107 | 
108 | **Type:** Basket (Read Only)
109 | 
110 | This method returns the current valid basket of the session customer or creates a new one if no current valid
111 |  basket exists. See getCurrentBasket() for more details.
112 |  
113 |  
114 |  In hybrid storefront scenarios (Phased Launch sites that utilize SFRA/SiteGenesis for some part while also
115 |  utilizing PWA Kit or other custom headless solution for another part of the same site), this method must
116 |  NOT be used. For these scenarios, create baskets via POST baskets REST calls.
117 | 
118 | ### storedBasket
119 | 
120 | **Type:** Basket (Read Only)
121 | 
122 | This method returns the stored basket of the session customer or null if none is found. A stored
123 |  basket is returned in the following situation:
124 |  
125 |  During one visit, a customer-Q logs in and creates a basket-A by adding products to it.
126 |  In a later visit, a second basket-B is created for a guest customer who then logs in as customer-Q. 
127 |  
128 |  In this case basket-B is reassigned to customer-Q and basket-A is accessible as the stored basket using this method. 
129 |  Now it is possible to merge the information from the stored basket to the active basket.
130 |  
131 |  A stored basket will exist only if the corresponding setting is selected in the Business Manager site
132 |  preferences' baskets section. A basket is valid for the configured basket lifetime.
133 |  
134 |  Typical usage:
135 | 
136 |   var currentBasket : Basket = BasketMgr.getCurrentOrNewBasket();
137 |  var storedBasket : Basket = BasketMgr.getStoredBasket();
138 |  if (storedBasket) {
139 |      // transfer all the data needed from the stored to the active basket
140 |  }
141 | 
142 | ### temporaryBaskets
143 | 
144 | **Type:** List (Read Only)
145 | 
146 | Retrieve all open temporary baskets for the logged in customer.
147 |  
148 |  Please notice that baskets are invalidated after a certain amount of time and may not be returned anymore.
149 | 
150 | ## Constructor Summary
151 | 
152 | ## Method Summary
153 | 
154 | ### createAgentBasket
155 | 
156 | **Signature:** `static createAgentBasket() : Basket`
157 | 
158 | Creates a new agent basket for the current session customer.
159 | 
160 | ### createBasketFromOrder
161 | 
162 | **Signature:** `static createBasketFromOrder(order : Order) : Basket`
163 | 
164 | Creates a Basket from an existing Order for the purposes of changing an Order.
165 | 
166 | ### createTemporaryBasket
167 | 
168 | **Signature:** `static createTemporaryBasket() : Basket`
169 | 
170 | Creates a new temporary basket for the current session customer.
171 | 
172 | ### deleteBasket
173 | 
174 | **Signature:** `static deleteBasket(basket : Basket) : void`
175 | 
176 | Remove a customer basket including a temporary basket.
177 | 
178 | ### deleteTemporaryBasket
179 | 
180 | **Signature:** `static deleteTemporaryBasket(basket : Basket) : void`
181 | 
182 | Remove a customer temporary basket.
183 | 
184 | ### getBasket
185 | 
186 | **Signature:** `static getBasket(uuid : String) : Basket`
187 | 
188 | This method returns a valid basket of the session customer or null if none is found.
189 | 
190 | ### getBaskets
191 | 
192 | **Signature:** `static getBaskets() : List`
193 | 
194 | Retrieve all open baskets for the logged in customer including the temporary baskets.
195 | 
196 | ### getCurrentBasket
197 | 
198 | **Signature:** `static getCurrentBasket() : Basket`
199 | 
200 | This method returns the current valid basket of the session customer or null if no current valid basket exists.
201 | 
202 | ### getCurrentOrNewBasket
203 | 
204 | **Signature:** `static getCurrentOrNewBasket() : Basket`
205 | 
206 | This method returns the current valid basket of the session customer or creates a new one if no current valid basket exists.
207 | 
208 | ### getStoredBasket
209 | 
210 | **Signature:** `static getStoredBasket() : Basket`
211 | 
212 | This method returns the stored basket of the session customer or null if none is found.
213 | 
214 | ### getTemporaryBasket
215 | 
216 | **Signature:** `static getTemporaryBasket(uuid : String) : Basket`
217 | 
218 | This method returns a valid temporary basket of the session customer or null if none is found.
219 | 
220 | ### getTemporaryBaskets
221 | 
222 | **Signature:** `static getTemporaryBaskets() : List`
223 | 
224 | Retrieve all open temporary baskets for the logged in customer.
225 | 
226 | ## Method Detail
227 | 
228 | ## Method Details
229 | 
230 | ### createAgentBasket
231 | 
232 | **Signature:** `static createAgentBasket() : Basket`
233 | 
234 | **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.
235 | 
236 | **Returns:**
237 | 
238 | the newly created basket for the customer which is logged in
239 | 
240 | **Throws:**
241 | 
242 | CreateAgentBasketLimitExceededException - indicates that no agent basket could be created because the agent basket limit is already exceeded
243 | 
244 | ---
245 | 
246 | ### createBasketFromOrder
247 | 
248 | **Signature:** `static createBasketFromOrder(order : Order) : Basket`
249 | 
250 | **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 } }
251 | 
252 | **Parameters:**
253 | 
254 | - `order`: Order to create a Basket for
255 | 
256 | **Returns:**
257 | 
258 | a new Basket
259 | 
260 | **See Also:**
261 | 
262 | AgentUserMgr.loginAgentUser(String, String)
263 | AgentUserMgr.loginOnBehalfOfCustomer(Customer)
264 | 
265 | **Throws:**
266 | 
267 | - `CreateBasketFromOrderException`: indicates the Order is in an invalid state.
268 | 
269 | ---
270 | 
271 | ### createTemporaryBasket
272 | 
273 | **Signature:** `static createTemporaryBasket() : Basket`
274 | 
275 | **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.
276 | 
277 | **Returns:**
278 | 
279 | the newly created basket for the current session customer
280 | 
281 | ---
282 | 
283 | ### deleteBasket
284 | 
285 | **Signature:** `static deleteBasket(basket : Basket) : void`
286 | 
287 | **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.
288 | 
289 | **Parameters:**
290 | 
291 | - `basket`: the basket to be removed
292 | 
293 | **See Also:**
294 | 
295 | AgentUserMgr.loginAgentUser(String, String)
296 | AgentUserMgr.loginOnBehalfOfCustomer(Customer)
297 | 
298 | ---
299 | 
300 | ### deleteTemporaryBasket
301 | 
302 | **Signature:** `static deleteTemporaryBasket(basket : Basket) : void`
303 | 
304 | **Description:** Remove a customer temporary basket.
305 | 
306 | **Parameters:**
307 | 
308 | - `basket`: the temporary basket to be removed
309 | 
310 | ---
311 | 
312 | ### getBasket
313 | 
314 | **Signature:** `static getBasket(uuid : String) : Basket`
315 | 
316 | **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().
317 | 
318 | **Parameters:**
319 | 
320 | - `uuid`: the id of the requested basket.
321 | 
322 | **Returns:**
323 | 
324 | the basket or null
325 | 
326 | ---
327 | 
328 | ### getBaskets
329 | 
330 | **Signature:** `static getBaskets() : List`
331 | 
332 | **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.
333 | 
334 | **Returns:**
335 | 
336 | all open baskets
337 | 
338 | ---
339 | 
340 | ### getCurrentBasket
341 | 
342 | **Signature:** `static getCurrentBasket() : Basket`
343 | 
344 | **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.
345 | 
346 | **Returns:**
347 | 
348 | the current basket or null if no valid current basket exists.
349 | 
350 | ---
351 | 
352 | ### getCurrentOrNewBasket
353 | 
354 | **Signature:** `static getCurrentOrNewBasket() : Basket`
355 | 
356 | **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.
357 | 
358 | **Returns:**
359 | 
360 | the basket, existing or newly created
361 | 
362 | ---
363 | 
364 | ### getStoredBasket
365 | 
366 | **Signature:** `static getStoredBasket() : Basket`
367 | 
368 | **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 }
369 | 
370 | **Returns:**
371 | 
372 | the stored basket or null if no valid stored basket exists.
373 | 
374 | ---
375 | 
376 | ### getTemporaryBasket
377 | 
378 | **Signature:** `static getTemporaryBasket(uuid : String) : Basket`
379 | 
380 | **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().
381 | 
382 | **Parameters:**
383 | 
384 | - `uuid`: the id of the requested temporary basket.
385 | 
386 | **Returns:**
387 | 
388 | the temporary basket or null
389 | 
390 | ---
391 | 
392 | ### getTemporaryBaskets
393 | 
394 | **Signature:** `static getTemporaryBaskets() : List`
395 | 
396 | **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.
397 | 
398 | **Returns:**
399 | 
400 | all open temporary baskets
401 | 
402 | ---
```

--------------------------------------------------------------------------------
/tests/mcp/node/search-system-object-attribute-groups.full-mode.programmatic.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Node.js programmatic tests for search_system_object_attribute_groups MCP tool (Full Mode)
  3 |  * 
  4 |  * These tests provide comprehensive validation of the tool's functionality in full mode
  5 |  * with real SFCC OCAPI integration, including complex query scenarios, error handling,
  6 |  * performance validation, and integration testing.
  7 |  * 
  8 |  * Test Categories:
  9 |  * 1. Basic Functionality Tests
 10 |  * 2. Complex Query Scenarios  
 11 |  * 3. Pagination and Sorting Tests
 12 |  * 4. Error Handling and Edge Cases
 13 |  * 5. Performance and Resource Management
 14 |  * 6. Integration and Data Consistency Tests
 15 |  * 7. Authentication and Security Tests
 16 |  */
 17 | 
 18 | import { strictEqual, ok } from 'assert';
 19 | import { describe, it, before, after, beforeEach } from 'node:test';
 20 | import { connect } from 'mcp-aegis';
 21 | 
 22 | describe('search_system_object_attribute_groups - Full Mode Comprehensive Tests', () => {
 23 |   let client;
 24 |   const configPath = './aegis.config.with-dw.json';
 25 | 
 26 |   before(async () => {
 27 |     client = await connect(configPath);
 28 |   });
 29 | 
 30 |   after(async () => {
 31 |     if (client?.connected) {
 32 |       await client.disconnect();
 33 |     }
 34 |   });
 35 | 
 36 |   beforeEach(() => {
 37 |     // CRITICAL: Clear all buffers to prevent leaking into next tests
 38 |     if (client?.clearAllBuffers) {
 39 |       client.clearAllBuffers();
 40 |     }
 41 |   });
 42 | 
 43 |   // Enhanced helper functions for complex validations
 44 |   function assertValidMCPResponse(result) {
 45 |     ok(result.content, 'Should have content');
 46 |     ok(Array.isArray(result.content), 'Content should be array');
 47 |     strictEqual(typeof result.isError, 'boolean', 'isError should be boolean');
 48 |   }
 49 | 
 50 |   function getTextContent(result) {
 51 |     assertValidMCPResponse(result);
 52 |     const textContent = result.content.find(c => c.type === 'text');
 53 |     ok(textContent, 'Should have text content');
 54 |     return textContent.text;
 55 |   }
 56 | 
 57 |   async function callTool(params) {
 58 |     const result = await client.callTool('search_system_object_attribute_groups', params);
 59 |     assertValidMCPResponse(result);
 60 |     return result;
 61 |   }
 62 | 
 63 |   describe('1. Basic Functionality Tests', () => {
 64 |     it('should be available in full mode', async () => {
 65 |       const tools = await client.listTools();
 66 |       const tool = tools.find(t => t.name === 'search_system_object_attribute_groups');
 67 |       ok(tool, 'Tool should be available in full mode');
 68 |       strictEqual(tool.name, 'search_system_object_attribute_groups');
 69 |       ok(tool.description.includes('Search attribute groups'), 'Tool should have proper description');
 70 |     });
 71 | 
 72 |     it('should return attribute groups for Product object type', async () => {
 73 |       const result = await callTool({
 74 |         objectType: 'Product',
 75 |         searchRequest: {
 76 |           query: { match_all_query: {} },
 77 |           count: 10
 78 |         }
 79 |       });
 80 | 
 81 |       const text = getTextContent(result);
 82 |       ok(text.includes('object_attribute_group'), 'Should mention object_attribute_group in response');
 83 |     });
 84 | 
 85 |     it('should return attribute groups for SitePreferences object type', async () => {
 86 |       const result = await callTool({
 87 |         objectType: 'SitePreferences',
 88 |         searchRequest: {
 89 |           query: { match_all_query: {} },
 90 |           count: 5
 91 |         }
 92 |       });
 93 | 
 94 |       const text = getTextContent(result);
 95 |       ok(text.includes('SitePreferences'), 'Should mention SitePreferences');
 96 |     });
 97 | 
 98 |     it('should handle Customer object type', async () => {
 99 |       const result = await callTool({
100 |         objectType: 'Customer',
101 |         searchRequest: {
102 |           query: { match_all_query: {} },
103 |           count: 5
104 |         }
105 |       });
106 | 
107 |       const text = getTextContent(result);
108 |       ok(text.length > 0, 'Should have some response content');
109 |     });
110 |   });
111 | 
112 |   describe('2. Complex Query Scenarios', () => {
113 |     it('should handle text_query for searching by display name', async () => {
114 |       const result = await callTool({
115 |         objectType: 'Product',
116 |         searchRequest: {
117 |           query: {
118 |             text_query: {
119 |               fields: ['display_name', 'description'],
120 |               search_phrase: 'product'
121 |             }
122 |           },
123 |           count: 5
124 |         }
125 |       });
126 | 
127 |       const text = getTextContent(result);
128 |       ok(text.length > 0, 'Should have response content');
129 |     });
130 | 
131 |     it('should handle term_query for exact field matching', async () => {
132 |       const result = await callTool({
133 |         objectType: 'Product',
134 |         searchRequest: {
135 |           query: {
136 |             term_query: {
137 |               fields: ['internal'],
138 |               operator: 'is',
139 |               values: ['false']
140 |             }
141 |           },
142 |           count: 10
143 |         }
144 |       });
145 | 
146 |       const text = getTextContent(result);
147 |       ok(text.length > 0, 'Should have response content');
148 |     });
149 | 
150 |     it('should handle complex bool_query with multiple conditions', async () => {
151 |       const result = await callTool({
152 |         objectType: 'Product',
153 |         searchRequest: {
154 |           query: {
155 |             bool_query: {
156 |               must: [
157 |                 {
158 |                   term_query: {
159 |                     fields: ['internal'],
160 |                     operator: 'is',
161 |                     values: ['false']
162 |                   }
163 |                 }
164 |               ],
165 |               must_not: [
166 |                 {
167 |                   text_query: {
168 |                     fields: ['id'],
169 |                     search_phrase: 'system'
170 |                   }
171 |                 }
172 |               ]
173 |             }
174 |           },
175 |           count: 5
176 |         }
177 |       });
178 | 
179 |       const text = getTextContent(result);
180 |       ok(text.length > 0, 'Should have response content');
181 |     });
182 |   });
183 | 
184 |   describe('3. Pagination and Sorting Tests', () => {
185 |     it('should handle pagination with start and count parameters', async () => {
186 |       const result1 = await callTool({
187 |         objectType: 'Product',
188 |         searchRequest: {
189 |           query: { match_all_query: {} },
190 |           start: 0,
191 |           count: 2
192 |         }
193 |       });
194 | 
195 |       const result2 = await callTool({
196 |         objectType: 'Product',
197 |         searchRequest: {
198 |           query: { match_all_query: {} },
199 |           start: 2,
200 |           count: 2
201 |         }
202 |       });
203 | 
204 |       const text1 = getTextContent(result1);
205 |       const text2 = getTextContent(result2);
206 | 
207 |       // Pages should be different (unless there are very few groups)
208 |       if (text1.includes('attribute groups') && text2.includes('attribute groups')) {
209 |         // Both pages have data, they might be different or the same if limited data
210 |         ok(true, 'Pagination works correctly');
211 |       }
212 |     });
213 | 
214 |     it('should handle sorting by different fields', async () => {
215 |       const ascResult = await callTool({
216 |         objectType: 'Product',
217 |         searchRequest: {
218 |           query: { match_all_query: {} },
219 |           sorts: [{ field: 'id', sort_order: 'asc' }],
220 |           count: 5
221 |         }
222 |       });
223 | 
224 |       const descResult = await callTool({
225 |         objectType: 'Product',
226 |         searchRequest: {
227 |           query: { match_all_query: {} },
228 |           sorts: [{ field: 'id', sort_order: 'desc' }],
229 |           count: 5
230 |         }
231 |       });
232 | 
233 |       const ascText = getTextContent(ascResult);
234 |       const descText = getTextContent(descResult);
235 | 
236 |       // Both should have content
237 |       ok(ascText.length > 0, 'Ascending sort should return data');
238 |       ok(descText.length > 0, 'Descending sort should return data');
239 |     });
240 | 
241 |     it('should handle multiple sort criteria', async () => {
242 |       const result = await callTool({
243 |         objectType: 'Product',
244 |         searchRequest: {
245 |           query: { match_all_query: {} },
246 |           sorts: [
247 |             { field: 'internal', sort_order: 'asc' },
248 |             { field: 'position', sort_order: 'desc' }
249 |           ],
250 |           count: 5
251 |         }
252 |       });
253 | 
254 |       const text = getTextContent(result);
255 |       ok(text.length > 0, 'Should return data');
256 |     });
257 |   });
258 | 
259 |   describe('4. Error Handling and Edge Cases', () => {
260 |     it('should handle invalid object type gracefully', async () => {
261 |       const result = await callTool({
262 |         objectType: 'InvalidObjectType',
263 |         searchRequest: {
264 |           query: { match_all_query: {} }
265 |         }
266 |       });
267 | 
268 |       const text = getTextContent(result).toLowerCase();
269 |       ok(
270 |         text.includes('error') || text.includes('not found') || text.includes('no') || 
271 |         text.includes('invalid') || text.includes('empty') || text.includes('0'),
272 |         'Should handle invalid object type appropriately'
273 |       );
274 |     });
275 | 
276 |     it('should handle missing required parameters', async () => {
277 |       try {
278 |         await client.callTool('search_system_object_attribute_groups', {
279 |           searchRequest: {
280 |             query: { match_all_query: {} }
281 |           }
282 |           // Missing objectType
283 |         });
284 |         ok(false, 'Should have thrown an error for missing objectType');
285 |       } catch (error) {
286 |         ok(error.message.includes('objectType') || error.message.includes('required'), 
287 |            'Error should mention missing objectType');
288 |       }
289 |     });
290 | 
291 |     it('should handle malformed query structures', async () => {
292 |       const result = await callTool({
293 |         objectType: 'Product',
294 |         searchRequest: {
295 |           query: {
296 |             text_query: {
297 |               // Missing required fields
298 |               search_phrase: 'test'
299 |             }
300 |           }
301 |         }
302 |       });
303 | 
304 |       const text = getTextContent(result);
305 |       ok(text.length > 0, 'Should have some response');
306 |     });
307 | 
308 |     it('should handle empty search results', async () => {
309 |       const result = await callTool({
310 |         objectType: 'Product',
311 |         searchRequest: {
312 |           query: {
313 |             text_query: {
314 |               fields: ['id'],
315 |               search_phrase: 'zzz_nonexistent_group_name_xyz'
316 |             }
317 |           }
318 |         }
319 |       });
320 | 
321 |       const text = getTextContent(result).toLowerCase();
322 |       ok(
323 |         text.includes('no') || text.includes('empty') || text.includes('not found') || 
324 |         text.includes('0') || text.includes('none'),
325 |         'Should indicate no results found'
326 |       );
327 |     });
328 | 
329 |     it('should handle very large count parameters', async () => {
330 |       const result = await callTool({
331 |         objectType: 'Product',
332 |         searchRequest: {
333 |           query: { match_all_query: {} },
334 |           count: 1000 // Very large count
335 |         }
336 |       });
337 | 
338 |       const text = getTextContent(result);
339 |       ok(text.length > 0, 'Should return data');
340 |     });
341 |   });
342 | 
343 |   describe('5. Performance and Resource Management Tests', () => {
344 |     it('should respond within reasonable time for simple queries', async () => {
345 |       const startTime = Date.now();
346 |       
347 |       const result = await callTool({
348 |         objectType: 'Product',
349 |         searchRequest: {
350 |           query: { match_all_query: {} },
351 |           count: 5
352 |         }
353 |       });
354 |       
355 |       const duration = Date.now() - startTime;
356 |       
357 |       ok(result.content, 'Should return result');
358 |       ok(duration < 5000, `Simple query should complete within 5 seconds, took ${duration}ms`);
359 |     });
360 | 
361 |     it('should handle concurrent requests efficiently', async () => {
362 |       const startTime = Date.now();
363 |       
364 |       const promises = Array.from({ length: 3 }, (_, i) => 
365 |         callTool({
366 |           objectType: 'Product',
367 |           searchRequest: {
368 |             query: { match_all_query: {} },
369 |             start: i * 2,
370 |             count: 2
371 |           }
372 |         })
373 |       );
374 |       
375 |       const results = await Promise.all(promises);
376 |       const duration = Date.now() - startTime;
377 |       
378 |       ok(results.length === 3, 'Should handle all concurrent requests');
379 |       results.forEach((result, index) => {
380 |         ok(result.content, `Request ${index} should have content`);
381 |       });
382 |       ok(duration < 10000, `Concurrent requests should complete within 10 seconds, took ${duration}ms`);
383 |     });
384 | 
385 |     it('should handle memory efficiently with large result sets', async () => {
386 |       const result = await callTool({
387 |         objectType: 'Product',
388 |         searchRequest: {
389 |           query: { match_all_query: {} },
390 |           count: 200 // Large result set
391 |         }
392 |       });
393 | 
394 |       const text = getTextContent(result);
395 |       ok(text.length > 0, 'Should have response content');
396 |       ok(text.length < 1000000, 'Response should be reasonable in size (< 1MB)');
397 |     });
398 |   });
399 | 
400 |   describe('6. Integration and Data Consistency Tests', () => {
401 |     it('should return consistent results for repeated identical queries', async () => {
402 |       const queryParams = {
403 |         objectType: 'Product',
404 |         searchRequest: {
405 |           query: { match_all_query: {} },
406 |           count: 5,
407 |           sorts: [{ field: 'id', sort_order: 'asc' }]
408 |         }
409 |       };
410 | 
411 |       const result1 = await callTool(queryParams);
412 |       const result2 = await callTool(queryParams);
413 | 
414 |       const text1 = getTextContent(result1);
415 |       const text2 = getTextContent(result2);
416 | 
417 |       // Results should be identical for same query
418 |       strictEqual(text1, text2, 'Repeated identical queries should return same results');
419 |     });
420 | 
421 |     it('should validate select parameter functionality', async () => {
422 |       const fullResult = await callTool({
423 |         objectType: 'Product',
424 |         searchRequest: {
425 |           query: { match_all_query: {} },
426 |           count: 3,
427 |           select: '(**)'
428 |         }
429 |       });
430 | 
431 |       const limitedResult = await callTool({
432 |         objectType: 'Product',
433 |         searchRequest: {
434 |           query: { match_all_query: {} },
435 |           count: 3,
436 |           select: '(data.(id))'
437 |         }
438 |       });
439 | 
440 |       const fullText = getTextContent(fullResult);
441 |       const limitedText = getTextContent(limitedResult);
442 | 
443 |       // Both should have content but limited might be shorter
444 |       ok(fullText.length > 0, 'Full select should return data');
445 |       ok(limitedText.length > 0, 'Limited select should return data');
446 |     });
447 | 
448 |     it('should handle cross-object type comparisons', async () => {
449 |       const productResult = await callTool({
450 |         objectType: 'Product',
451 |         searchRequest: {
452 |           query: { match_all_query: {} },
453 |           count: 3
454 |         }
455 |       });
456 | 
457 |       const sitePrefsResult = await callTool({
458 |         objectType: 'SitePreferences',
459 |         searchRequest: {
460 |           query: { match_all_query: {} },
461 |           count: 3
462 |         }
463 |       });
464 | 
465 |       const productText = getTextContent(productResult);
466 |       const sitePrefsText = getTextContent(sitePrefsResult);
467 | 
468 |       // Results should be different (different object types should have different groups)
469 |       ok(productText.includes('Product') || productText.includes('attribute'), 
470 |          'Product result should be relevant to products');
471 |       ok(sitePrefsText.includes('SitePreferences') || sitePrefsText.includes('attribute'), 
472 |          'SitePreferences result should be relevant to site preferences');
473 |     });
474 |   });
475 | 
476 |   describe('7. Authentication and Security Tests', () => {
477 |     it('should require valid OCAPI credentials', async () => {
478 |       // This test verifies that the tool properly uses authentication
479 |       // The fact that we can call the tool successfully means auth is working
480 |       const result = await callTool({
481 |         objectType: 'Product',
482 |         searchRequest: {
483 |           query: { match_all_query: {} },
484 |           count: 1
485 |         }
486 |       });
487 | 
488 |       const text = getTextContent(result).toLowerCase();
489 |       
490 |       // Should not indicate authentication errors
491 |       ok(!text.includes('unauthorized') && !text.includes('authentication'), 
492 |          'Should not have authentication errors with valid credentials');
493 |     });
494 | 
495 |     it('should handle input sanitization properly', async () => {
496 |       // Test with potentially problematic input characters
497 |       const result = await callTool({
498 |         objectType: 'Product',
499 |         searchRequest: {
500 |           query: {
501 |             text_query: {
502 |               fields: ['id'],
503 |               search_phrase: '<script>alert("test")</script>'
504 |             }
505 |           },
506 |           count: 1
507 |         }
508 |       });
509 | 
510 |       const text = getTextContent(result);
511 |       
512 |       // The query echo should be properly escaped (showing backslashes for quotes)
513 |       // and should not contain executable script content in the actual data hits
514 |       ok(text.includes('\\"test\\"'), 'Should properly escape quotes in query echo');
515 |       
516 |       // Check that the hits section doesn't contain the script content
517 |       const jsonResponse = JSON.parse(text);
518 |       if (jsonResponse.hits && jsonResponse.hits.length > 0) {
519 |         const hitsText = JSON.stringify(jsonResponse.hits);
520 |         ok(!hitsText.includes('<script>'), 'Actual data hits should not contain script content');
521 |       }
522 |     });
523 | 
524 |     it('should respect OCAPI rate limiting and security constraints', async () => {
525 |       // Make multiple rapid requests to test rate limiting handling
526 |       const promises = Array.from({ length: 5 }, () => 
527 |         callTool({
528 |           objectType: 'Product',
529 |           searchRequest: {
530 |             query: { match_all_query: {} },
531 |             count: 1
532 |           }
533 |         })
534 |       );
535 | 
536 |       try {
537 |         const results = await Promise.all(promises);
538 |         
539 |         // All requests should succeed or fail gracefully
540 |         results.forEach((result, index) => {
541 |           ok(result.content, `Request ${index} should have content or proper error handling`);
542 |         });
543 |       } catch (error) {
544 |         // If rate limited, should fail gracefully
545 |         ok(error.message.includes('rate') || error.message.includes('limit') || 
546 |            error.message.includes('too many'), 
547 |            'Rate limiting should be handled gracefully');
548 |       }
549 |     });
550 |   });
551 | 
552 |   describe('8. Edge Case and Robustness Tests', () => {
553 |     it('should handle extremely specific search criteria', async () => {
554 |       const result = await callTool({
555 |         objectType: 'Product',
556 |         searchRequest: {
557 |           query: {
558 |             bool_query: {
559 |               must: [
560 |                 {
561 |                   term_query: {
562 |                     fields: ['internal'],
563 |                     operator: 'is',
564 |                     values: ['false']
565 |                   }
566 |                 },
567 |                 {
568 |                   text_query: {
569 |                     fields: ['display_name'],
570 |                     search_phrase: 'custom'
571 |                   }
572 |                 }
573 |               ]
574 |             }
575 |           },
576 |           count: 1
577 |         }
578 |       });
579 | 
580 |       const text = getTextContent(result);
581 |       ok(text.length > 0, 'Should have response content');
582 |     });
583 | 
584 |     it('should handle boundary values for pagination', async () => {
585 |       // Test with start = 0
586 |       const zeroStart = await callTool({
587 |         objectType: 'Product',
588 |         searchRequest: {
589 |           query: { match_all_query: {} },
590 |           start: 0,
591 |           count: 1
592 |         }
593 |       });
594 | 
595 |       // Test with count = 1
596 |       const minCount = await callTool({
597 |         objectType: 'Product',
598 |         searchRequest: {
599 |           query: { match_all_query: {} },
600 |           start: 0,
601 |           count: 1
602 |         }
603 |       });
604 | 
605 |       ok(zeroStart.content, 'Should handle start=0');
606 |       ok(minCount.content, 'Should handle count=1');
607 |     });
608 | 
609 |     it('should maintain consistent response format across different scenarios', async () => {
610 |       const scenarios = [
611 |         {
612 |           name: 'match_all',
613 |           params: {
614 |             objectType: 'Product',
615 |             searchRequest: { query: { match_all_query: {} }, count: 2 }
616 |           }
617 |         },
618 |         {
619 |           name: 'text_search',
620 |           params: {
621 |             objectType: 'Product',
622 |             searchRequest: {
623 |               query: {
624 |                 text_query: {
625 |                   fields: ['id'],
626 |                   search_phrase: 'product'
627 |                 }
628 |               },
629 |               count: 2
630 |             }
631 |           }
632 |         },
633 |         {
634 |           name: 'with_sorting',
635 |           params: {
636 |             objectType: 'Product',
637 |             searchRequest: {
638 |               query: { match_all_query: {} },
639 |               sorts: [{ field: 'id', sort_order: 'asc' }],
640 |               count: 2
641 |             }
642 |           }
643 |         }
644 |       ];
645 | 
646 |       for (const scenario of scenarios) {
647 |         const result = await callTool(scenario.params);
648 |         
649 |         assertValidMCPResponse(result);
650 |         
651 |         const textContent = result.content.find(c => c.type === 'text');
652 |         ok(textContent, `${scenario.name} should have text content`);
653 |         ok(typeof textContent.text === 'string', `${scenario.name} should have string text`);
654 |       }
655 |     });
656 |   });
657 | });
```

--------------------------------------------------------------------------------
/tests/referenced-types-extractor.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ReferencedTypesExtractor } from '../src/clients/docs/referenced-types-extractor.js';
  2 | 
  3 | describe('ReferencedTypesExtractor', () => {
  4 |   describe('extractReferencedTypes', () => {
  5 |     it('should extract types from property definitions', () => {
  6 |       const content = `
  7 | # Class Product
  8 | 
  9 | ## Properties
 10 | 
 11 | ### price
 12 | **Type:** Money
 13 | 
 14 | The price of the product.
 15 | 
 16 | ### category
 17 | **Type:** Category
 18 | 
 19 | The product category.
 20 |       `;
 21 | 
 22 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
 23 | 
 24 |       expect(result).toContain('Money');
 25 |       expect(result).toContain('Category');
 26 |       expect(result).toHaveLength(2);
 27 |     });
 28 | 
 29 |     it('should extract return types from method signatures', () => {
 30 |       const content = `
 31 | # Class Product
 32 | 
 33 | ## Methods
 34 | 
 35 | ### getPrice(): Money
 36 | Returns the product price.
 37 | 
 38 | ### getCategory(): Category
 39 | Returns the product category.
 40 | 
 41 | ### getName(): String
 42 | Returns the product name.
 43 |       `;
 44 | 
 45 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
 46 | 
 47 |       expect(result).toContain('Money');
 48 |       expect(result).toContain('Category');
 49 |       expect(result).toContain('String');
 50 |       expect(result).toHaveLength(3);
 51 |     });
 52 | 
 53 |     it('should extract parameter types from method signatures', () => {
 54 |       const content = `
 55 | # Class Product
 56 | 
 57 | ## Methods
 58 | 
 59 | ### setPrice(price: Money): void
 60 | Sets the product price.
 61 | 
 62 | ### addToCategory(category: Category, primary: Boolean): void
 63 | Adds product to category.
 64 | 
 65 | ### updateInventory(record: InventoryRecord, quantity: Number): Boolean
 66 | Updates inventory record.
 67 |       `;
 68 | 
 69 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
 70 | 
 71 |       expect(result).toContain('Money');
 72 |       expect(result).toContain('Category');
 73 |       expect(result).toContain('Boolean');
 74 |       expect(result).toContain('InventoryRecord');
 75 |       expect(result).toContain('Number');
 76 |       expect(result).toHaveLength(5);
 77 |     });
 78 | 
 79 |     it('should extract types with dots (fully qualified names)', () => {
 80 |       const content = `
 81 | # Class Product
 82 | 
 83 | ## Properties
 84 | 
 85 | ### site
 86 | **Type:** dw.system.Site
 87 | 
 88 | The site this product belongs to.
 89 | 
 90 | ## Methods
 91 | 
 92 | ### getCustomer(): dw.customer.Customer
 93 | Returns the customer.
 94 | 
 95 | ### processOrder(order: dw.order.Order): dw.system.Status
 96 | Processes an order.
 97 |       `;
 98 | 
 99 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
100 | 
101 |       expect(result).toContain('dw.system.Site');
102 |       expect(result).toContain('dw.customer.Customer');
103 |       expect(result).toContain('dw.order.Order');
104 |       expect(result).toContain('dw.system.Status');
105 |       expect(result).toHaveLength(4);
106 |     });
107 | 
108 |     it('should handle complex method signatures with multiple parameters', () => {
109 |       const content = `
110 | # Class OrderProcessor
111 | 
112 | ## Methods
113 | 
114 | ### processPayment(order: Order, payment: PaymentInstrument, amount: Money): PaymentStatus
115 | Processes payment for an order.
116 | 
117 | ### createShipment(order: Order, items: Collection, address: OrderAddress): Shipment
118 | Creates a shipment.
119 |       `;
120 | 
121 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
122 | 
123 |       expect(result).toContain('Order');
124 |       expect(result).toContain('PaymentInstrument');
125 |       expect(result).toContain('Money');
126 |       expect(result).toContain('PaymentStatus');
127 |       expect(result).toContain('Collection');
128 |       expect(result).toContain('OrderAddress');
129 |       expect(result).toContain('Shipment');
130 |       expect(result).toHaveLength(7);
131 |     });
132 | 
133 |     it('should ignore primitive types and lowercase types', () => {
134 |       const content = `
135 | # Class Product
136 | 
137 | ## Properties
138 | 
139 | ### name
140 | **Type:** string
141 | 
142 | Product name.
143 | 
144 | ### active
145 | **Type:** boolean
146 | 
147 | Whether product is active.
148 | 
149 | ### count
150 | **Type:** number
151 | 
152 | Product count.
153 | 
154 | ### data
155 | **Type:** object
156 | 
157 | Product data.
158 | 
159 | ## Methods
160 | 
161 | ### isValid(): boolean
162 | Returns validity.
163 | 
164 | ### process(data: object, flag: boolean): string
165 | Processes data.
166 |       `;
167 | 
168 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
169 | 
170 |       // Should not include primitive types (string, boolean, number, object)
171 |       expect(result).not.toContain('string');
172 |       expect(result).not.toContain('boolean');
173 |       expect(result).not.toContain('number');
174 |       expect(result).not.toContain('object');
175 |       expect(result).toHaveLength(0);
176 |     });
177 | 
178 |     it('should handle mixed case and special formatting', () => {
179 |       const content = `
180 | # Class ProductManager
181 | 
182 | ## Properties
183 | 
184 | ### defaultCategory
185 | **Type:**   Category
186 | 
187 | Default category with extra spaces.
188 | 
189 | ### primarySite
190 | **Type:**dw.system.Site
191 | 
192 | Site without space after colon.
193 | 
194 | ## Methods
195 | 
196 | ### getCollection():  Collection
197 | Returns collection with extra spaces.
198 | 
199 | ### updateStatus( status: ProductStatus ): Boolean
200 | Method with spaced parameters.
201 |       `;
202 | 
203 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
204 | 
205 |       expect(result).toContain('Category');
206 |       expect(result).toContain('dw.system.Site');
207 |       expect(result).toContain('Collection');
208 |       expect(result).toContain('ProductStatus');
209 |       expect(result).toContain('Boolean');
210 |       expect(result).toHaveLength(5);
211 |     });
212 | 
213 |     it('should handle empty content', () => {
214 |       const result = ReferencedTypesExtractor.extractReferencedTypes('');
215 |       expect(result).toEqual([]);
216 |     });
217 | 
218 |     it('should handle content with no type references', () => {
219 |       const content = `
220 | # Class Product
221 | 
222 | This is a product class without any type references.
223 | 
224 | It has some text but no property types or method signatures.
225 |       `;
226 | 
227 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
228 |       expect(result).toEqual([]);
229 |     });
230 | 
231 |     it('should handle malformed content gracefully', () => {
232 |       const content = `
233 | # Class Product
234 | 
235 | **Type:** 
236 | 
237 | ### method():
238 | Returns something.
239 | 
240 | ### invalid(: Missing): 
241 | Malformed signature.
242 |       `;
243 | 
244 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
245 |       // The regex will match "Missing" from the parameter, which starts with uppercase
246 |       expect(result).toContain('Missing');
247 |       expect(result).toHaveLength(1);
248 |     });
249 | 
250 |     it('should deduplicate repeated type references', () => {
251 |       const content = `
252 | # Class Product
253 | 
254 | ## Properties
255 | 
256 | ### price
257 | **Type:** Money
258 | 
259 | ### discountPrice  
260 | **Type:** Money
261 | 
262 | ## Methods
263 | 
264 | ### getPrice(): Money
265 | Returns price.
266 | 
267 | ### setPrice(amount: Money): void
268 | Sets price.
269 |       `;
270 | 
271 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
272 | 
273 |       expect(result).toContain('Money');
274 |       expect(result.filter(type => type === 'Money')).toHaveLength(1);
275 |       expect(result).toHaveLength(1);
276 |     });
277 |   });
278 | 
279 |   describe('isSFCCType', () => {
280 |     it('should identify uppercase types as SFCC types', () => {
281 |       // Access private method through any for testing
282 |       const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType;
283 | 
284 |       expect(isSFCCType('Product')).toBe(true);
285 |       expect(isSFCCType('Money')).toBe(true);
286 |       expect(isSFCCType('Category')).toBe(true);
287 |       expect(isSFCCType('Boolean')).toBe(true);
288 |       expect(isSFCCType('String')).toBe(true);
289 |       expect(isSFCCType('Number')).toBe(true);
290 |     });
291 | 
292 |     it('should identify dotted types as SFCC types', () => {
293 |       const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType;
294 | 
295 |       expect(isSFCCType('dw.catalog.Product')).toBe(true);
296 |       expect(isSFCCType('dw.system.Site')).toBe(true);
297 |       expect(isSFCCType('dw.customer.Customer')).toBe(true);
298 |       expect(isSFCCType('dw.order.Order')).toBe(true);
299 |     });
300 | 
301 |     it('should reject lowercase primitive types', () => {
302 |       const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType;
303 | 
304 |       expect(isSFCCType('string')).toBe(false);
305 |       expect(isSFCCType('boolean')).toBe(false);
306 |       expect(isSFCCType('number')).toBe(false);
307 |       expect(isSFCCType('object')).toBe(false);
308 |       expect(isSFCCType('function')).toBe(false);
309 |       expect(isSFCCType('undefined')).toBe(false);
310 |       expect(isSFCCType('null')).toBe(false);
311 |     });
312 | 
313 |     it('should reject types starting with lowercase', () => {
314 |       const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType;
315 | 
316 |       expect(isSFCCType('productType')).toBe(false);
317 |       expect(isSFCCType('categoryId')).toBe(false);
318 |       expect(isSFCCType('data')).toBe(false);
319 |       expect(isSFCCType('value')).toBe(false);
320 |     });
321 | 
322 |     it('should handle edge cases', () => {
323 |       const isSFCCType = (ReferencedTypesExtractor as any).isSFCCType;
324 | 
325 |       expect(isSFCCType('')).toBe(false);
326 |       expect(isSFCCType('A')).toBe(true);
327 |       expect(isSFCCType('a')).toBe(false);
328 |       expect(isSFCCType('1Product')).toBe(false); // starts with number
329 |       expect(isSFCCType('_Product')).toBe(false); // starts with underscore
330 |     });
331 |   });
332 | 
333 |   describe('filterCircularReferences', () => {
334 |     it('should filter out exact class name matches', () => {
335 |       const referencedTypes = ['Product', 'Category', 'Money', 'Product'];
336 |       const currentClassName = 'Product';
337 | 
338 |       const result = ReferencedTypesExtractor.filterCircularReferences(
339 |         referencedTypes,
340 |         currentClassName,
341 |       );
342 | 
343 |       expect(result).toContain('Category');
344 |       expect(result).toContain('Money');
345 |       expect(result).not.toContain('Product');
346 |       expect(result).toHaveLength(2);
347 |     });
348 | 
349 |     it('should filter out fully qualified class name matches', () => {
350 |       const referencedTypes = [
351 |         'dw.catalog.Product',
352 |         'dw.system.Site',
353 |         'Category',
354 |         'Money',
355 |       ];
356 |       const currentClassName = 'Product';
357 | 
358 |       const result = ReferencedTypesExtractor.filterCircularReferences(
359 |         referencedTypes,
360 |         currentClassName,
361 |       );
362 | 
363 |       expect(result).toContain('dw.system.Site');
364 |       expect(result).toContain('Category');
365 |       expect(result).toContain('Money');
366 |       expect(result).not.toContain('dw.catalog.Product');
367 |       expect(result).toHaveLength(3);
368 |     });
369 | 
370 |     it('should preserve types that do not create circular references', () => {
371 |       const referencedTypes = [
372 |         'Category',
373 |         'Money',
374 |         'ProductVariant', // Different from Product
375 |         'dw.system.Site',
376 |         'Boolean',
377 |       ];
378 |       const currentClassName = 'Product';
379 | 
380 |       const result = ReferencedTypesExtractor.filterCircularReferences(
381 |         referencedTypes,
382 |         currentClassName,
383 |       );
384 | 
385 |       expect(result).toEqual([
386 |         'Category',
387 |         'Money',
388 |         'ProductVariant',
389 |         'dw.system.Site',
390 |         'Boolean',
391 |       ]);
392 |       expect(result).toHaveLength(5);
393 |     });
394 | 
395 |     it('should handle empty input arrays', () => {
396 |       const result = ReferencedTypesExtractor.filterCircularReferences([], 'Product');
397 |       expect(result).toEqual([]);
398 |     });
399 | 
400 |     it('should handle case where all types are circular references', () => {
401 |       const referencedTypes = [
402 |         'Product',
403 |         'dw.catalog.Product',
404 |         'some.namespace.Product',
405 |       ];
406 |       const currentClassName = 'Product';
407 | 
408 |       const result = ReferencedTypesExtractor.filterCircularReferences(
409 |         referencedTypes,
410 |         currentClassName,
411 |       );
412 | 
413 |       expect(result).toEqual([]);
414 |     });
415 | 
416 |     it('should be case sensitive', () => {
417 |       const referencedTypes = ['product', 'PRODUCT', 'Product'];
418 |       const currentClassName = 'Product';
419 | 
420 |       const result = ReferencedTypesExtractor.filterCircularReferences(
421 |         referencedTypes,
422 |         currentClassName,
423 |       );
424 | 
425 |       expect(result).toContain('product');
426 |       expect(result).toContain('PRODUCT');
427 |       expect(result).not.toContain('Product');
428 |       expect(result).toHaveLength(2);
429 |     });
430 | 
431 |     it('should handle complex namespace scenarios', () => {
432 |       const referencedTypes = [
433 |         'dw.catalog.Category',
434 |         'dw.order.OrderItem', // Different class
435 |         'com.custom.Product', // Different namespace but same class
436 |         'Product', // Exact match
437 |       ];
438 |       const currentClassName = 'Product';
439 | 
440 |       const result = ReferencedTypesExtractor.filterCircularReferences(
441 |         referencedTypes,
442 |         currentClassName,
443 |       );
444 | 
445 |       expect(result).toContain('dw.catalog.Category');
446 |       expect(result).toContain('dw.order.OrderItem');
447 |       expect(result).not.toContain('com.custom.Product'); // Should be filtered
448 |       expect(result).not.toContain('Product'); // Should be filtered
449 |       expect(result).toHaveLength(2);
450 |     });
451 |   });
452 | 
453 |   describe('extractFilteredReferencedTypes', () => {
454 |     it('should extract and filter types in one operation', () => {
455 |       const content = `
456 | # Class Product
457 | 
458 | ## Properties
459 | 
460 | ### price
461 | **Type:** Money
462 | 
463 | ### category
464 | **Type:** Category
465 | 
466 | ### relatedProduct
467 | **Type:** Product
468 | 
469 | ## Methods
470 | 
471 | ### getPrice(): Money
472 | Returns price.
473 | 
474 | ### getRelatedProduct(): Product
475 | Returns related product.
476 | 
477 | ### createProduct(name: String): Product
478 | Creates a new product.
479 |       `;
480 | 
481 |       const result = ReferencedTypesExtractor.extractFilteredReferencedTypes(
482 |         content,
483 |         'Product',
484 |       );
485 | 
486 |       expect(result).toContain('Money');
487 |       expect(result).toContain('Category');
488 |       expect(result).toContain('String');
489 |       expect(result).not.toContain('Product'); // Should be filtered out
490 |       expect(result).toHaveLength(3);
491 |     });
492 | 
493 |     it('should handle complex scenarios with multiple filtering needs', () => {
494 |       const content = `
495 | # Class OrderProcessor
496 | 
497 | ## Properties
498 | 
499 | ### defaultOrder
500 | **Type:** dw.order.Order
501 | 
502 | ### processor
503 | **Type:** OrderProcessor
504 | 
505 | ## Methods
506 | 
507 | ### processOrder(order: dw.order.Order): OrderProcessor
508 | Processes an order.
509 | 
510 | ### createProcessor(): OrderProcessor
511 | Creates processor instance.
512 | 
513 | ### validatePayment(payment: PaymentInstrument): Boolean
514 | Validates payment.
515 |       `;
516 | 
517 |       const result = ReferencedTypesExtractor.extractFilteredReferencedTypes(
518 |         content,
519 |         'OrderProcessor',
520 |       );
521 | 
522 |       expect(result).toContain('dw.order.Order');
523 |       expect(result).toContain('PaymentInstrument');
524 |       expect(result).toContain('Boolean');
525 |       expect(result).not.toContain('OrderProcessor'); // Should be filtered out
526 |       expect(result).toHaveLength(3);
527 |     });
528 | 
529 |     it('should work with empty content', () => {
530 |       const result = ReferencedTypesExtractor.extractFilteredReferencedTypes(
531 |         '',
532 |         'Product',
533 |       );
534 |       expect(result).toEqual([]);
535 |     });
536 | 
537 |     it('should work when no types need filtering', () => {
538 |       const content = `
539 | # Class Product
540 | 
541 | ## Properties
542 | 
543 | ### price
544 | **Type:** Money
545 | 
546 | ### category
547 | **Type:** Category
548 | 
549 | ## Methods
550 | 
551 | ### getInventory(): InventoryRecord
552 | Returns inventory.
553 |       `;
554 | 
555 |       const result = ReferencedTypesExtractor.extractFilteredReferencedTypes(
556 |         content,
557 |         'Product',
558 |       );
559 | 
560 |       expect(result).toContain('Money');
561 |       expect(result).toContain('Category');
562 |       expect(result).toContain('InventoryRecord');
563 |       expect(result).toHaveLength(3);
564 |     });
565 | 
566 |     it('should work when all types need filtering', () => {
567 |       const content = `
568 | # Class Product
569 | 
570 | ## Properties
571 | 
572 | ### self
573 | **Type:** Product
574 | 
575 | ## Methods
576 | 
577 | ### getSelf(): Product
578 | Returns self.
579 | 
580 | ### createProduct(): dw.catalog.Product
581 | Creates product.
582 |       `;
583 | 
584 |       const result = ReferencedTypesExtractor.extractFilteredReferencedTypes(
585 |         content,
586 |         'Product',
587 |       );
588 | 
589 |       expect(result).toEqual([]);
590 |     });
591 |   });
592 | 
593 |   describe('integration tests', () => {
594 |     it('should handle realistic SFCC class documentation', () => {
595 |       const content = `
596 | # Class dw.catalog.Product
597 | 
598 | The Product class represents a product in the catalog.
599 | 
600 | ## Properties
601 | 
602 | ### ID
603 | **Type:** String
604 | 
605 | The product ID.
606 | 
607 | ### name
608 | **Type:** String  
609 | 
610 | The product name.
611 | 
612 | ### primaryCategory
613 | **Type:** Category
614 | 
615 | The primary category for this product.
616 | 
617 | ### priceModel
618 | **Type:** ProductPriceModel
619 | 
620 | The price model for this product.
621 | 
622 | ### availabilityModel
623 | **Type:** ProductAvailabilityModel
624 | 
625 | The availability model for this product.
626 | 
627 | ## Methods
628 | 
629 | ### getID(): String
630 | Returns the product ID.
631 | 
632 | ### getName(): String
633 | Returns the product name.
634 | 
635 | ### getPrimaryCategory(): Category
636 | Returns the primary category.
637 | 
638 | ### getPriceModel(): ProductPriceModel
639 | Returns the price model.
640 | 
641 | ### getAvailabilityModel(): ProductAvailabilityModel  
642 | Returns the availability model.
643 | 
644 | ### setName(name: String): void
645 | Sets the product name.
646 | 
647 | ### assignToCategory(category: Category, primary: Boolean): void
648 | Assigns the product to a category.
649 | 
650 | ### getVariationModel(): ProductVariationModel
651 | Returns the variation model.
652 | 
653 | ### isVariant(): Boolean
654 | Returns true if this is a variant product.
655 | 
656 | ### getMasterProduct(): Product
657 | Returns the master product if this is a variant.
658 | 
659 | ### getVariants(): Collection
660 | Returns all variants of this master product.
661 |       `;
662 | 
663 |       const result = ReferencedTypesExtractor.extractFilteredReferencedTypes(
664 |         content,
665 |         'Product',
666 |       );
667 | 
668 |       expect(result).toContain('String');
669 |       expect(result).toContain('Category');
670 |       expect(result).toContain('ProductPriceModel');
671 |       expect(result).toContain('ProductAvailabilityModel');
672 |       expect(result).toContain('Boolean');
673 |       expect(result).toContain('ProductVariationModel');
674 |       expect(result).toContain('Collection');
675 |       expect(result).not.toContain('Product'); // Should be filtered out
676 |       expect(result).toHaveLength(7);
677 |     });
678 | 
679 |     it('should handle documentation with inheritance information', () => {
680 |       const content = `
681 | # Class ProductVariant extends Product
682 | 
683 | A product variant inherits from Product.
684 | 
685 | ## Properties
686 | 
687 | ### masterProduct
688 | **Type:** Product
689 | 
690 | The master product.
691 | 
692 | ### variationAttributes
693 | **Type:** Map
694 | 
695 | The variation attributes.
696 | 
697 | ## Methods
698 | 
699 | ### getMasterProduct(): Product
700 | Returns the master product.
701 | 
702 | ### getVariationValue(attribute: ProductAttribute): ProductAttributeValue
703 | Returns variation value.
704 | 
705 | ### isOnline(): Boolean
706 | Checks if variant is online.
707 |       `;
708 | 
709 |       const result = ReferencedTypesExtractor.extractFilteredReferencedTypes(
710 |         content,
711 |         'ProductVariant',
712 |       );
713 | 
714 |       expect(result).toContain('Map');
715 |       expect(result).toContain('ProductAttribute');
716 |       expect(result).toContain('ProductAttributeValue');
717 |       expect(result).toContain('Boolean');
718 |       expect(result).toContain('Product'); // Will be included since it's extracted from content
719 |       expect(result).not.toContain('ProductVariant'); // Should be filtered as self-reference
720 |       expect(result).toHaveLength(5);
721 |     });
722 |   });
723 | 
724 |   describe('error handling and edge cases', () => {
725 |     it('should handle null input gracefully', () => {
726 |       expect(() => {
727 |         ReferencedTypesExtractor.extractReferencedTypes(null as any);
728 |       }).toThrow();
729 |     });
730 | 
731 |     it('should handle undefined input gracefully', () => {
732 |       expect(() => {
733 |         ReferencedTypesExtractor.extractReferencedTypes(undefined as any);
734 |       }).toThrow();
735 |     });
736 | 
737 |     it('should handle very large content efficiently', () => {
738 |       const largeContent = `
739 | # Class Product
740 | 
741 | ## Properties
742 | 
743 | ### prop1
744 | **Type:** Type1
745 | 
746 | `.repeat(1000);
747 | 
748 |       const result = ReferencedTypesExtractor.extractReferencedTypes(largeContent);
749 |       expect(result).toContain('Type1');
750 |       expect(result).toHaveLength(1);
751 |     });
752 | 
753 |     it('should handle content with special characters', () => {
754 |       const content = `
755 | # Class Product
756 | 
757 | ## Properties
758 | 
759 | ### price€
760 | **Type:** Money€
761 | 
762 | European price.
763 | 
764 | ### nameÜnicode
765 | **Type:** String
766 | 
767 | Unicode name.
768 | 
769 | ## Methods
770 | 
771 | ### getPrice€(): Money€
772 | Returns European price.
773 |       `;
774 | 
775 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
776 | 
777 |       // The current regex pattern [A-Za-z0-9.] doesn't include Unicode characters
778 |       // So Money€ will be extracted as just "Money"
779 |       expect(result).toContain('Money'); // Money€ gets truncated to Money
780 |       expect(result).toContain('String');
781 |       expect(result).toHaveLength(2);
782 |     });
783 | 
784 |     it('should handle malformed markdown gracefully', () => {
785 |       const content = `
786 | # Class Product
787 | 
788 | **Type:** Category
789 | This is not a proper property definition.
790 | 
791 | ### method(: Money
792 | Malformed method signature.
793 | 
794 | **Type:**
795 | Empty type.
796 | 
797 | ### valid
798 | **Type:** ValidType
799 | 
800 | This should work.
801 |       `;
802 | 
803 |       const result = ReferencedTypesExtractor.extractReferencedTypes(content);
804 |       // The extractor will find:
805 |       // - "Category" from "**Type:** Category" (matches property pattern)
806 |       // - "Money" from "method(: Money" (matches return type pattern)
807 |       // - "ValidType" from the proper property definition
808 |       expect(result).toContain('Category');
809 |       expect(result).toContain('Money');
810 |       expect(result).toContain('ValidType');
811 |       expect(result).toHaveLength(3);
812 |     });
813 |   });
814 | });
815 | 
```

--------------------------------------------------------------------------------
/tests/mcp/node/search-sfcc-methods.docs-only.programmatic.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Programmatic tests for search_sfcc_methods tool
  3 |  * 
  4 |  * These tests provide advanced verification capabilities beyond YAML pattern matching,
  5 |  * including performance monitoring, dynamic validation, error categorization,
  6 |  * comprehensive response structure analysis, and method signature validation.
  7 |  * 
  8 |  * Response format discovered via aegis query:
  9 |  * - Success: { content: [{ type: "text", text: "[{\"className\": \"...\", \"method\": {...}}, ...]" }] }
 10 |  * - Empty: { content: [{ type: "text", text: "[]" }] }
 11 |  * - Error: { content: [{ type: "text", text: "Error: ..." }], isError: true }
 12 |  * 
 13 |  * Method object structure:
 14 |  * {
 15 |  *   "className": "dw_util.Calendar",
 16 |  *   "method": {
 17 |  *     "name": "get",
 18 |  *     "signature": "get(field : Number) : Number",
 19 |  *     "description": "Returns the value of the given calendar field."
 20 |  *   }
 21 |  * }
 22 |  */
 23 | 
 24 | import { test, describe, before, after, beforeEach } from 'node:test';
 25 | import { strict as assert } from 'node:assert';
 26 | import { connect } from 'mcp-aegis';
 27 | 
 28 | /**
 29 |  * Performance monitoring utility class
 30 |  */
 31 | 
 32 | /**
 33 |  * Method signature analysis utility class
 34 |  */
 35 | class MethodSignatureAnalyzer {
 36 |   constructor() {
 37 |     this.patterns = {
 38 |       staticMethod: /^static\s+/,
 39 |       returnType: /:\s*([A-Za-z0-9_[\]]+)\s*$/,
 40 |       parameters: /\(([^)]*)\)/,
 41 |       methodName: /^(?:static\s+)?([A-Za-z_][A-Za-z0-9_]*)/
 42 |     };
 43 |   }
 44 | 
 45 |   analyzeSignature(signature) {
 46 |     return {
 47 |       isStatic: this.patterns.staticMethod.test(signature),
 48 |       returnType: this.extractReturnType(signature),
 49 |       parameters: this.extractParameters(signature),
 50 |       methodName: this.extractMethodName(signature),
 51 |       isValid: this.validateSignature(signature)
 52 |     };
 53 |   }
 54 | 
 55 |   extractReturnType(signature) {
 56 |     const match = signature.match(this.patterns.returnType);
 57 |     return match ? match[1] : null;
 58 |   }
 59 | 
 60 |   extractParameters(signature) {
 61 |     const match = signature.match(this.patterns.parameters);
 62 |     if (!match || !match[1].trim()) return [];
 63 |     
 64 |     return match[1].split(',').map(param => {
 65 |       const parts = param.trim().split(/\s*:\s*/);
 66 |       return {
 67 |         name: parts[0]?.trim() || '',
 68 |         type: parts[1]?.trim() || ''
 69 |       };
 70 |     });
 71 |   }
 72 | 
 73 |   extractMethodName(signature) {
 74 |     const match = signature.match(this.patterns.methodName);
 75 |     return match ? match[1] : null;
 76 |   }
 77 | 
 78 |   validateSignature(signature) {
 79 |     // Basic validation - should have method name, parentheses, and return type
 80 |     return signature.includes('(') && 
 81 |            signature.includes(')') && 
 82 |            signature.includes(':') &&
 83 |            signature.trim().length > 0;
 84 |   }
 85 | 
 86 |   categorizeMethod(methodData) {
 87 |     const { signature } = methodData.method;
 88 |     const analysis = this.analyzeSignature(signature);
 89 |     
 90 |     const categories = [];
 91 |     
 92 |     if (analysis.isStatic) categories.push('static');
 93 |     if (analysis.methodName?.startsWith('get')) categories.push('getter');
 94 |     if (analysis.methodName?.startsWith('set')) categories.push('setter');
 95 |     if (analysis.methodName?.startsWith('is') || analysis.methodName?.startsWith('has')) categories.push('boolean');
 96 |     if (analysis.returnType === 'void') categories.push('void');
 97 |     if (analysis.parameters.length === 0) categories.push('parameterless');
 98 |     if (analysis.parameters.length > 3) categories.push('complex');
 99 |     
100 |     return categories;
101 |   }
102 | }
103 | 
104 | describe('search_sfcc_methods Programmatic Tests', () => {
105 |   let client;
106 |   const signatureAnalyzer = new MethodSignatureAnalyzer();
107 | 
108 |   before(async () => {
109 |     client = await connect('./aegis.config.docs-only.json');
110 |   });
111 | 
112 |   after(async () => {
113 |     if (client?.connected) {
114 |       await client.disconnect();
115 |     }
116 |   });
117 | 
118 |   beforeEach(() => {
119 |     // CRITICAL: Clear all buffers to prevent test interference
120 |     client.clearAllBuffers(); // Recommended - comprehensive protection
121 |   });
122 | 
123 |   describe('Protocol Compliance', () => {
124 |     test('should be properly connected to MCP server', async () => {
125 |       assert.ok(client.connected, 'Client should be connected');
126 |     });
127 | 
128 |     test('should have search_sfcc_methods tool available', async () => {
129 |       const tools = await client.listTools();
130 |       const searchTool = tools.find(tool => tool.name === 'search_sfcc_methods');
131 |       
132 |       assert.ok(searchTool, 'search_sfcc_methods tool should be available');
133 |       assert.equal(searchTool.name, 'search_sfcc_methods');
134 |       assert.ok(searchTool.description, 'Tool should have description');
135 |       assert.ok(searchTool.inputSchema, 'Tool should have input schema');
136 |       assert.equal(searchTool.inputSchema.type, 'object');
137 |       assert.ok(searchTool.inputSchema.properties.methodName, 'Tool should require methodName parameter');
138 |     });
139 |   });
140 | 
141 |   describe('Response Structure Validation', () => {
142 |     test('should return properly structured MCP response for valid method search', async () => {
143 |       const result = await client.callTool('search_sfcc_methods', { methodName: 'get' });
144 |       
145 |       // Validate MCP response structure
146 |       assertValidMCPResponse(result);
147 |       assert.equal(result.isError, false, 'Should not be an error response');
148 |       assert.equal(result.content.length, 1, 'Should have exactly one content item');
149 |       assert.equal(result.content[0].type, 'text', 'Content should be text type');
150 |       
151 |       // Validate JSON array structure
152 |       const methodArray = parseMethodArray(result.content[0].text);
153 |       assert.ok(Array.isArray(methodArray), 'Response should contain valid JSON array');
154 |       assert.ok(methodArray.length > 0, 'Should return at least one method for get query');
155 |       
156 |       // Validate method object structure
157 |       methodArray.forEach(methodData => {
158 |         assert.equal(typeof methodData, 'object', 'Each method should be an object');
159 |         assert.ok(methodData.className, 'Method should have className property');
160 |         assert.ok(methodData.method, 'Method should have method property');
161 |         assert.equal(typeof methodData.className, 'string', 'className should be string');
162 |         assert.equal(typeof methodData.method, 'object', 'method should be object');
163 |         
164 |         // Validate method object properties
165 |         assert.ok(methodData.method.name, 'Method should have name');
166 |         assert.ok(methodData.method.signature, 'Method should have signature');
167 |         assert.ok(methodData.method.description, 'Method should have description');
168 |         assert.equal(typeof methodData.method.name, 'string', 'Method name should be string');
169 |         assert.equal(typeof methodData.method.signature, 'string', 'Method signature should be string');
170 |         assert.equal(typeof methodData.method.description, 'string', 'Method description should be string');
171 |       });
172 |       
173 |       // Performance validation (lenient for CI environments)
174 |     });
175 | 
176 |     test('should return empty array for no matches', async () => {
177 |       const result = await client.callTool(
178 |         'search_sfcc_methods', { methodName: 'zzznothingfound' }
179 |       );
180 |       
181 |       assertValidMCPResponse(result);
182 |       assert.equal(result.isError, false, 'Should not be an error response');
183 |       
184 |       const methodArray = parseMethodArray(result.content[0].text);
185 |       assert.ok(Array.isArray(methodArray), 'Response should be valid JSON array');
186 |       assert.equal(methodArray.length, 0, 'Should return empty array for no matches');
187 |       
188 |       // Performance should be reasonable for no results (lenient for CI)
189 |     });
190 | 
191 |     test('should return error response for invalid parameters', async () => {
192 |       const result = await client.callTool('search_sfcc_methods', { methodName: '' }
193 |       );
194 |       
195 |       assertValidMCPResponse(result);
196 |       assert.equal(result.isError, true, 'Should be an error response');
197 |       assert.ok(result.content[0].text.includes('Error:'), 'Should contain error message');
198 |       assert.ok(result.content[0].text.includes('non-empty string'), 'Should specify validation requirement');
199 |       
200 |       // Error responses should be reasonably fast (CI-friendly)
201 |     });
202 |   });
203 | 
204 |   describe('Method Search Functionality', () => {
205 |     const commonMethodQueries = [
206 |       { query: 'get', expectedMin: 50, category: 'getter' },
207 |       { query: 'set', expectedMin: 10, category: 'setter' },
208 |       { query: 'create', expectedMin: 5, category: 'factory' },
209 |       { query: 'toString', expectedMin: 20, category: 'conversion' },
210 |       { query: 'getValue', expectedMin: 10, category: 'accessor' },
211 |       { query: 'getName', expectedMin: 5, category: 'accessor' }
212 |     ];
213 | 
214 |     commonMethodQueries.forEach(({ query, expectedMin, category }) => {
215 |       test(`should find relevant methods for ${category} query: "${query}"`, async () => {
216 |         const result = await client.callTool(
217 |           'search_sfcc_methods', { methodName: query }
218 |         );
219 |         
220 |         assertValidMCPResponse(result);
221 |         assert.equal(result.isError, false, 'Should not be an error');
222 |         
223 |         const methodArray = parseMethodArray(result.content[0].text);
224 |         assert.ok(methodArray.length >= expectedMin, 
225 |           `Should find at least ${expectedMin} methods for "${query}", found ${methodArray.length}`);
226 |         
227 |         // Validate relevance - all method names should contain the query term
228 |         methodArray.forEach(methodData => {
229 |           const methodName = methodData.method.name.toLowerCase();
230 |           const lowerQuery = query.toLowerCase();
231 |           assert.ok(methodName.includes(lowerQuery), 
232 |             `Method "${methodData.method.name}" should contain query term "${query}"`);
233 |         });
234 |         
235 |         // Validate class name format
236 |         methodArray.forEach(methodData => {
237 |           assert.ok(
238 |             methodData.className.startsWith('dw_') || 
239 |             methodData.className.startsWith('TopLevel.') || 
240 |             methodData.className.startsWith('best-practices.') || 
241 |             methodData.className.startsWith('sfra.'),
242 |             `Class name "${methodData.className}" should start with recognized namespace`
243 |           );
244 |         });
245 |       });
246 |     });
247 |   });
248 | 
249 |   describe('Method Signature Analysis', () => {
250 |     test('should return valid method signatures for all results', async () => {
251 |       const result = await client.callTool('search_sfcc_methods', { methodName: 'get' });
252 |       
253 |       assertValidMCPResponse(result);
254 |       const methodArray = parseMethodArray(result.content[0].text);
255 |       
256 |       methodArray.slice(0, 20).forEach(methodData => { // Test first 20 for performance
257 |         const analysis = signatureAnalyzer.analyzeSignature(methodData.method.signature);
258 |         
259 |         assert.ok(analysis.isValid, 
260 |           `Method signature "${methodData.method.signature}" should be valid`);
261 |         assert.ok(analysis.methodName, 
262 |           `Should extract method name from "${methodData.method.signature}"`);
263 |         assert.ok(analysis.returnType, 
264 |           `Should extract return type from "${methodData.method.signature}"`);
265 |         
266 |         // Method name in signature should match the method name property
267 |         assert.equal(analysis.methodName, methodData.method.name,
268 |           'Method name in signature should match method.name property');
269 |         
270 |         // Signature should be properly formatted
271 |         assert.ok(methodData.method.signature.includes('('), 'Signature should contain opening parenthesis');
272 |         assert.ok(methodData.method.signature.includes(')'), 'Signature should contain closing parenthesis');
273 |         assert.ok(methodData.method.signature.includes(':'), 'Signature should contain return type separator');
274 |       });
275 |     });
276 | 
277 |     test('should categorize methods correctly by signature patterns', async () => {
278 |       const result = await client.callTool('search_sfcc_methods', { methodName: 'get' });
279 |       
280 |       assertValidMCPResponse(result);
281 |       const methodArray = parseMethodArray(result.content[0].text);
282 |       
283 |       const categoryCounts = {
284 |         static: 0,
285 |         getter: 0,
286 |         parameterless: 0,
287 |         complex: 0
288 |       };
289 |       
290 |       methodArray.slice(0, 50).forEach(methodData => {
291 |         const categories = signatureAnalyzer.categorizeMethod(methodData);
292 |         categories.forEach(category => {
293 |           if (Object.prototype.hasOwnProperty.call(categoryCounts, category)) {
294 |             categoryCounts[category]++;
295 |           }
296 |         });
297 |       });
298 |       
299 |       // Should find a good mix of method types
300 |       assert.ok(categoryCounts.getter > 10, 'Should find plenty of getter methods');
301 |       // Static methods might not be common in 'get' search, so make this more flexible
302 |       assert.ok(categoryCounts.static >= 0, 'Static method count should be non-negative');
303 |       assert.ok(categoryCounts.parameterless > 5, 'Should find parameterless methods');
304 |       
305 |       assert.ok(Object.keys(categoryCounts).length > 0, 'Should have categorized methods');
306 |     });
307 |   });
308 | 
309 |   describe('Edge Case Validation', () => {
310 |     const edgeCases = [
311 |       { methodName: 'A', description: 'single character' },
312 |       { methodName: 'get', description: 'common method prefix' },
313 |       { methodName: 'GET', description: 'uppercase query' },
314 |       { methodName: 'getValue', description: 'compound method name' },
315 |       { methodName: '123', description: 'numeric query' },
316 |       { methodName: 'xyz_nonexistent_method', description: 'clearly non-existent method' }
317 |     ];
318 | 
319 |     edgeCases.forEach(({ methodName, description }) => {
320 |       test(`should handle ${description} query: "${methodName}"`, async () => {
321 |         const result = await client.callTool(
322 |           'search_sfcc_methods', { methodName }
323 |         );
324 |         
325 |         assertValidMCPResponse(result);
326 |         assert.equal(result.isError, false, 'Should not be an error for valid string');
327 |         
328 |         const methodArray = parseMethodArray(result.content[0].text);
329 |         assert.ok(Array.isArray(methodArray), 'Should return valid array');
330 |         
331 |         // All results should have valid structure
332 |         methodArray.forEach(methodData => {
333 |           assert.ok(methodData.className, 'Should have className');
334 |           assert.ok(methodData.method, 'Should have method object');
335 |           assert.ok(methodData.method.name, 'Should have method name');
336 |           assert.ok(methodData.method.signature, 'Should have method signature');
337 |           assert.ok(methodData.method.description, 'Should have method description');
338 |           
339 |           // Method name should contain the query (case insensitive)
340 |           const methodNameLower = methodData.method.name.toLowerCase();
341 |           const queryLower = methodName.toLowerCase();
342 |           assert.ok(methodNameLower.includes(queryLower), 
343 |             `Method "${methodData.method.name}" should contain query "${methodName}"`);
344 |         });
345 |         
346 |         // Performance should be reasonable for CI environments
347 |       });
348 |     });
349 |   });
350 | 
351 |   describe('Error Handling Validation', () => {
352 |     const errorCases = [
353 |       { args: { methodName: '' }, description: 'empty method name' },
354 |       { args: {}, description: 'missing methodName parameter' },
355 |       { args: { methodName: '   ' }, description: 'whitespace-only method name' },
356 |       { args: { methodName: null }, description: 'null method name' },
357 |       { args: { methodName: 123 }, description: 'non-string method name (number)' },
358 |       { args: { methodName: true }, description: 'non-string method name (boolean)' },
359 |       { args: { methodName: [] }, description: 'non-string method name (array)' },
360 |       { args: { methodName: {} }, description: 'non-string method name (object)' }
361 |     ];
362 | 
363 |     errorCases.forEach(({ args, description }) => {
364 |       test(`should return error for ${description}`, async () => {
365 |         const result = await client.callTool(
366 |           'search_sfcc_methods', args
367 |         );
368 |         
369 |         assertValidMCPResponse(result);
370 |         assert.equal(result.isError, true, 'Should be an error response');
371 |         assert.ok(result.content[0].text.includes('Error'), 'Should contain error message');
372 |         
373 |         // Categorize error type
374 |         const errorType = categorizeError(result.content[0].text);
375 |         assert.ok(['validation', 'not_found', 'unknown'].includes(errorType), 
376 |           `Error should be categorized (got: ${errorType})`);
377 |         
378 |         // Error responses should be reasonably fast (CI-friendly)
379 |       });
380 |     });
381 |   });
382 | 
383 |   describe('Consistency and Reliability', () => {
384 |     test('should return consistent results across multiple calls', async () => {
385 |       const methodName = 'getValue';
386 |       const results = await Promise.all([
387 |         client.callTool('search_sfcc_methods', { methodName }),
388 |         client.callTool('search_sfcc_methods', { methodName }),
389 |         client.callTool('search_sfcc_methods', { methodName })
390 |       ]);
391 |       
392 |       // All results should be successful
393 |       results.forEach(result => {
394 |         assertValidMCPResponse(result);
395 |         assert.equal(result.isError, false, 'Should not be error');
396 |       });
397 |       
398 |       // Parse arrays for comparison
399 |       const arrays = results.map(result => parseMethodArray(result.content[0].text));
400 |       
401 |       // All arrays should be identical
402 |       assert.deepEqual(arrays[0], arrays[1], 'Results should be consistent across calls');
403 |       assert.deepEqual(arrays[1], arrays[2], 'Results should be consistent across calls');
404 |     });
405 | 
406 |     test('should validate method data integrity and format', async () => {
407 |       const result = await client.callTool('search_sfcc_methods', { methodName: 'toString' });
408 |       
409 |       assertValidMCPResponse(result);
410 |       const methodArray = parseMethodArray(result.content[0].text);
411 |       
412 |       methodArray.forEach(methodData => {
413 |         // Validate method data integrity
414 |         assert.ok(methodData.className.length > 3, 
415 |           `Class name "${methodData.className}" should be reasonable length`);
416 |         assert.ok(methodData.className.length < 100, 
417 |           `Class name "${methodData.className}" should not be excessively long`);
418 |         
419 |         // Validate class name format
420 |         assert.match(methodData.className, /^(dw_|TopLevel\.|best-practices\.|sfra\.)[a-zA-Z0-9_./-]+$/, 
421 |           `Class name "${methodData.className}" should follow valid pattern`);
422 |         
423 |         // Validate method name format
424 |         assert.match(methodData.method.name, /^[a-zA-Z_][a-zA-Z0-9_]*$/, 
425 |           `Method name "${methodData.method.name}" should follow valid identifier pattern`);
426 |         
427 |         // Should not contain HTML or special characters
428 |         assert.ok(!methodData.method.name.includes('<'), 'Method name should not contain HTML');
429 |         assert.ok(!methodData.method.name.includes('>'), 'Method name should not contain HTML');
430 |         
431 |         // Description should be reasonable length
432 |         assert.ok(methodData.method.description.length > 5, 'Description should not be too short');
433 |         assert.ok(methodData.method.description.length < 2000, 'Description should not be excessively long');
434 |         
435 |         // Signature should contain the method name
436 |         assert.ok(methodData.method.signature.includes(methodData.method.name), 
437 |           'Signature should contain the method name');
438 |       });
439 |     });
440 |   });
441 | });
442 | 
443 | // Helper functions
444 | 
445 | /**
446 |  * Validates that a response follows proper MCP structure
447 |  */
448 | function assertValidMCPResponse(result) {
449 |   assert.ok(result.content, 'Response should have content property');
450 |   assert.ok(Array.isArray(result.content), 'Content should be an array');
451 |   assert.ok(result.content.length > 0, 'Content array should not be empty');
452 |   assert.equal(result.content[0].type, 'text', 'First content item should be text type');
453 |   assert.equal(typeof result.content[0].text, 'string', 'Text content should be a string');
454 |   
455 |   // isError property should always be present and boolean
456 |   assert.ok(Object.prototype.hasOwnProperty.call(result, 'isError'), 'isError property should always be present');
457 |   assert.equal(typeof result.isError, 'boolean', 'isError should be a boolean');
458 | }
459 | 
460 | /**
461 |  * Parses the method array from the response text
462 |  */
463 | function parseMethodArray(text) {
464 |   try {
465 |     return JSON.parse(text);
466 |   } catch {
467 |     throw new Error(`Failed to parse method array from response: ${text}`);
468 |   }
469 | }
470 | 
471 | /**
472 |  * Categorizes error messages by type
473 |  */
474 | function categorizeError(errorText) {
475 |   const errorPatterns = [
476 |     { type: 'validation', keywords: ['required', 'invalid', 'missing', 'non-empty', 'string'] },
477 |     { type: 'not_found', keywords: ['not found', 'does not exist'] },
478 |     { type: 'permission', keywords: ['permission', 'unauthorized', 'forbidden'] },
479 |     { type: 'network', keywords: ['connection', 'timeout', 'unreachable'] }
480 |   ];
481 | 
482 |   const lowerText = errorText.toLowerCase();
483 |   for (const pattern of errorPatterns) {
484 |     if (pattern.keywords.some(keyword => lowerText.includes(keyword))) {
485 |       return pattern.type;
486 |     }
487 |   }
488 |   return 'unknown';
489 | }
490 | 
```

--------------------------------------------------------------------------------
/tests/mcp/node/search-job-logs.full-mode.programmatic.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { test, describe, before, after, beforeEach } from 'node:test';
  2 | import { strict as assert } from 'node:assert';
  3 | import { connect } from 'mcp-aegis';
  4 | 
  5 | describe('search_job_logs - Optimized Programmatic Tests', () => {
  6 |   let client;
  7 |   let discoveredJobNames = [];
  8 |   let discoveredPatterns = [];
  9 | 
 10 |   before(async () => {
 11 |     client = await connect('./aegis.config.with-dw.json');
 12 |     
 13 |     // Discover available job names and patterns for advanced testing
 14 |     await discoverJobNames();
 15 |     await discoverCommonPatterns();
 16 |   });
 17 | 
 18 |   after(async () => {
 19 |     if (client?.connected) {
 20 |       await client.disconnect();
 21 |     }
 22 |   });
 23 | 
 24 |   beforeEach(() => {
 25 |     // CRITICAL: Clear all buffers to prevent leaking into next tests
 26 |     client.clearAllBuffers(); // Recommended - comprehensive protection
 27 |   });
 28 | 
 29 |   // Helper functions for dynamic discovery and complex validation
 30 |   async function discoverJobNames() {
 31 |     try {
 32 |       const result = await client.callTool('search_job_logs', { 
 33 |         pattern: 'Executing',
 34 |         limit: 50
 35 |       });
 36 |       
 37 |       if (!result.isError && result.content?.[0]?.text) {
 38 |         const text = parseResponseText(result.content[0].text);
 39 |         // Extract job names from the beginning of each log entry (first bracket pair)
 40 |         const jobMatches = text.match(/^\[([A-Za-z][A-Za-z0-9_-]*)\]/gm) || [];
 41 |         discoveredJobNames = [...new Set(jobMatches.map(match => match.slice(1, -1)))];
 42 |       }
 43 |     } catch (error) {
 44 |       console.warn('Could not discover job names:', error.message);
 45 |     }
 46 |   }
 47 | 
 48 |   async function discoverCommonPatterns() {
 49 |     const commonTerms = ['INFO', 'ERROR', 'step', 'completed', 'job', 'Executing'];
 50 |     
 51 |     for (const term of commonTerms) {
 52 |       try {
 53 |         const result = await client.callTool('search_job_logs', { 
 54 |           pattern: term,
 55 |           limit: 1
 56 |         });
 57 |         
 58 |         if (!result.isError && result.content?.[0]?.text) {
 59 |           const text = parseResponseText(result.content[0].text);
 60 |           if (!text.includes('No matches found')) {
 61 |             discoveredPatterns.push(term);
 62 |           }
 63 |         }
 64 |       } catch (error) {
 65 |         console.warn(`Error testing pattern "${term}":`, error.message);
 66 |       }
 67 |     }
 68 |   }
 69 | 
 70 |   // Helper functions for complex validations
 71 |   function assertValidMCPResponse(result) {
 72 |     assert.ok(result.content, 'Should have content');
 73 |     assert.ok(Array.isArray(result.content), 'Content should be array');
 74 |     assert.equal(typeof result.isError, 'boolean', 'isError should be boolean');
 75 |   }
 76 | 
 77 |   function parseResponseText(text) {
 78 |     // The response may come wrapped in quotes, so parse if needed
 79 |     return text.startsWith('"') && text.endsWith('"') 
 80 |       ? JSON.parse(text) 
 81 |       : text;
 82 |   }
 83 | 
 84 |   function assertSearchResultsFormat(result, pattern, expectedJobName = null) {
 85 |     assertValidMCPResponse(result);
 86 |     assert.equal(result.isError, false, 'Should not be an error response');
 87 |     assert.equal(result.content[0].type, 'text');
 88 |     
 89 |     const text = parseResponseText(result.content[0].text);
 90 |     
 91 |     if (text.includes('No matches found')) {
 92 |       // Valid empty result case
 93 |       assert.ok(text.includes(`No matches found for "${pattern}"`),
 94 |         `Should indicate no matches for pattern "${pattern}"`);
 95 |       return { matchCount: 0, entries: [] };
 96 |     }
 97 |     
 98 |     // Should contain found matches header
 99 |     if (expectedJobName) {
100 |       assert.ok(text.includes(`Found`) && text.includes(`matches for "${pattern}"`),
101 |         `Should contain found matches header for pattern "${pattern}"`);
102 |       assert.ok(text.includes(`job: ${expectedJobName}`) || text.includes(`in job: ${expectedJobName}`),
103 |         `Should indicate filtering by job "${expectedJobName}"`);
104 |     } else {
105 |       assert.ok(text.includes(`Found`) && text.includes(`matches for "${pattern}"`),
106 |         `Should contain found matches header for pattern "${pattern}"`);
107 |     }
108 |     
109 |     // Extract job log entries
110 |     const entries = extractJobLogEntries(text);
111 |     
112 |     // Validate each entry has proper structure and contains pattern
113 |     for (const entry of entries) {
114 |       // Each entry should have job name in brackets
115 |       assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()),
116 |         `Entry should start with job name in brackets: "${entry.substring(0, 50)}..."`);
117 |       
118 |       // Should contain timestamp in GMT format
119 |       assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(entry),
120 |         `Entry should contain GMT timestamp: "${entry.substring(0, 100)}..."`);
121 |       
122 |       // Should contain the search pattern (case-insensitive check)
123 |       assert.ok(entry.toLowerCase().includes(pattern.toLowerCase()),
124 |         `Entry should contain pattern "${pattern}": "${entry.substring(0, 100)}..."`);
125 |       
126 |       // If filtering by job name, all entries should be from that job
127 |       if (expectedJobName) {
128 |         assert.ok(entry.includes(`[${expectedJobName}]`),
129 |           `Entry should be from job "${expectedJobName}": "${entry.substring(0, 50)}..."`);
130 |       }
131 |     }
132 |     
133 |     // Extract match count from header
134 |     const matchCountMatch = text.match(/Found (\d+) matches/);
135 |     const matchCount = matchCountMatch ? parseInt(matchCountMatch[1]) : entries.length;
136 |     
137 |     return { matchCount, entries };
138 |   }
139 | 
140 |   function extractJobLogEntries(text) {
141 |     // Split by double newlines and filter for entries that look like job logs
142 |     const lines = text.split(/\n\n+/);
143 |     return lines.filter(line => 
144 |       line.trim() && 
145 |       /^\[[\w\-_]+\]/.test(line.trim()) &&
146 |       /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(line)
147 |     );
148 |   }
149 | 
150 |   function assertTimestampFormat(entries) {
151 |     for (const entry of entries) {
152 |       // Should contain valid timestamp format
153 |       const timestampMatch = entry.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT)/);
154 |       assert.ok(timestampMatch, `Entry should contain valid timestamp: "${entry.substring(0, 100)}..."`);
155 |       
156 |       // Validate timestamp is parseable
157 |       const timestampStr = timestampMatch[1].replace(' GMT', 'Z');
158 |       const date = new Date(timestampStr);
159 |       assert.ok(!isNaN(date.getTime()), `Timestamp should be valid date: "${timestampMatch[1]}"`);
160 |     }
161 |   }
162 | 
163 |   // === Dynamic Discovery and Validation Tests ===
164 |   describe('Dynamic Discovery and Validation', () => {
165 |     test('should dynamically validate discovered job names', async () => {
166 |       if (discoveredJobNames.length === 0) {
167 |         console.warn('No job names discovered - skipping dynamic validation');
168 |         return;
169 |       }
170 | 
171 |       // Test a sample of discovered job names (not all to avoid excessive testing)
172 |       const sampleJobs = discoveredJobNames.slice(0, Math.min(3, discoveredJobNames.length));
173 |       
174 |       for (const jobName of sampleJobs) {
175 |         const result = await client.callTool('search_job_logs', { 
176 |           pattern: 'Executing',
177 |           jobName: jobName,
178 |           limit: 1
179 |         });
180 |         
181 |         assertValidMCPResponse(result);
182 |         assert.equal(result.isError, false, `Job "${jobName}" should be searchable`);
183 |         
184 |         const searchResults = assertSearchResultsFormat(result, 'Executing', jobName);
185 |         
186 |         // If we found entries, they should all be from the specified job
187 |         if (searchResults.entries.length > 0) {
188 |           for (const entry of searchResults.entries) {
189 |             assert.ok(entry.includes(`[${jobName}]`),
190 |               `Entry should be from job "${jobName}": "${entry.substring(0, 50)}..."`);
191 |           }
192 |         }
193 |       }
194 |     });
195 | 
196 |     test('should dynamically test discovered patterns with complex validation', async () => {
197 |       if (discoveredPatterns.length === 0) {
198 |         console.warn('No patterns discovered - skipping dynamic pattern testing');
199 |         return;
200 |       }
201 | 
202 |       for (const pattern of discoveredPatterns) {
203 |         const result = await client.callTool('search_job_logs', { 
204 |           pattern: pattern,
205 |           limit: 3
206 |         });
207 |         
208 |         assertValidMCPResponse(result);
209 |         assert.equal(result.isError, false, `Pattern "${pattern}" should be searchable`);
210 |         
211 |         const searchResults = assertSearchResultsFormat(result, pattern);
212 |         
213 |         if (searchResults.entries.length > 0) {
214 |           // Validate complex content structure for each discovered pattern
215 |           assertTimestampFormat(searchResults.entries);
216 |           
217 |           // Validate pattern appears in content with case-insensitive search
218 |           for (const entry of searchResults.entries) {
219 |             assert.ok(entry.toLowerCase().includes(pattern.toLowerCase()),
220 |               `Entry should contain pattern "${pattern}" (case-insensitive): "${entry.substring(0, 100)}..."`);
221 |           }
222 |         }
223 |       }
224 |     });
225 | 
226 |     test('should validate cross-job pattern distribution', async () => {
227 |       if (discoveredJobNames.length < 2 || discoveredPatterns.length === 0) {
228 |         console.warn('Insufficient data for cross-job validation - skipping');
229 |         return;
230 |       }
231 | 
232 |       // Test if common patterns appear across multiple jobs
233 |       const commonPattern = discoveredPatterns[0]; // Use first discovered pattern
234 |       const jobResults = new Map();
235 |       
236 |       // Search each job for the common pattern
237 |       for (const jobName of discoveredJobNames.slice(0, 3)) {
238 |         const result = await client.callTool('search_job_logs', { 
239 |           pattern: commonPattern,
240 |           jobName: jobName,
241 |           limit: 1
242 |         });
243 |         
244 |         assertValidMCPResponse(result);
245 |         const searchResults = assertSearchResultsFormat(result, commonPattern, jobName);
246 |         jobResults.set(jobName, searchResults.matchCount);
247 |       }
248 |       
249 |       // Should be able to search across multiple jobs successfully
250 |       assert.ok(jobResults.size > 0, 'Should be able to search multiple jobs');
251 |     });
252 |   });
253 | 
254 |   // === Complex Content and Format Validation ===
255 |   describe('Complex Content Validation', () => {
256 |     test('should validate comprehensive job log entry structure', async () => {
257 |       const result = await client.callTool('search_job_logs', { 
258 |         pattern: 'step',
259 |         limit: 5
260 |       });
261 |       
262 |       assertValidMCPResponse(result);
263 |       const searchResults = assertSearchResultsFormat(result, 'step');
264 |       
265 |       if (searchResults.entries.length > 0) {
266 |         for (const entry of searchResults.entries) {
267 |           // Complex structural validation
268 |           assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()),
269 |             `Entry should start with job name: "${entry.substring(0, 50)}..."`);
270 |           
271 |           // Validate timestamp format and parseability
272 |           const timestampMatch = entry.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT)/);
273 |           assert.ok(timestampMatch, `Entry should contain timestamp: "${entry.substring(0, 100)}..."`);
274 |           
275 |           const timestampStr = timestampMatch[1].replace(' GMT', 'Z');
276 |           const date = new Date(timestampStr);
277 |           assert.ok(!isNaN(date.getTime()), `Timestamp should be parseable: "${timestampMatch[1]}"`);
278 |           
279 |           // Validate log level presence
280 |           assert.ok(/\s(INFO|ERROR|WARN|DEBUG)\s/.test(entry),
281 |             `Entry should contain log level: "${entry.substring(0, 100)}..."`);
282 |           
283 |           // Validate thread information
284 |           assert.ok(/Thread|SystemJobThread/.test(entry),
285 |             `Entry should contain thread info: "${entry.substring(0, 100)}..."`);
286 |         }
287 |       }
288 |     });
289 | 
290 |     test('should maintain consistent format across different search parameters', async () => {
291 |       const testCombinations = [
292 |         { pattern: 'INFO', level: 'info' },
293 |         { pattern: 'step', limit: 2 },
294 |         { pattern: 'completed', jobName: discoveredJobNames[0] || 'ImportCatalog' }
295 |       ];
296 |       
297 |       for (const combo of testCombinations) {
298 |         const result = await client.callTool('search_job_logs', combo);
299 |         
300 |         assertValidMCPResponse(result);
301 |         assert.equal(result.isError, false, `Combination ${JSON.stringify(combo)} should succeed`);
302 |         
303 |         const searchResults = assertSearchResultsFormat(result, combo.pattern, combo.jobName);
304 |         
305 |         if (searchResults.entries.length > 0) {
306 |           // Validate consistent structure regardless of search parameters
307 |           for (const entry of searchResults.entries) {
308 |             assert.ok(/^\[[\w\-_]+\]/.test(entry.trim()),
309 |               `Consistent format for combo ${JSON.stringify(combo)}: "${entry.substring(0, 50)}..."`);
310 |             assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(entry),
311 |               `Consistent timestamp for combo ${JSON.stringify(combo)}: "${entry.substring(0, 100)}..."`);
312 |           }
313 |         }
314 |       }
315 |     });
316 |   });
317 | 
318 |   // === Advanced Multi-Step Scenarios ===
319 |   describe('Advanced Multi-Step Scenarios', () => {
320 |     test('should support complex search refinement workflows', async () => {
321 |       // Step 1: Broad search to find available data
322 |       const broadResult = await client.callTool('search_job_logs', { 
323 |         pattern: 'job',
324 |         limit: 10
325 |       });
326 |       
327 |       assertValidMCPResponse(broadResult);
328 |       const broadSearch = assertSearchResultsFormat(broadResult, 'job');
329 |       
330 |       if (broadSearch.entries.length > 0) {
331 |         // Step 2: Extract job name from first result for targeted search
332 |         const firstEntry = broadSearch.entries[0];
333 |         const jobMatch = firstEntry.match(/^\[([^\]]+)\]/);
334 |         
335 |         if (jobMatch) {
336 |           const extractedJobName = jobMatch[1];
337 |           
338 |           // Step 3: Refined search using extracted job name
339 |           const refinedResult = await client.callTool('search_job_logs', { 
340 |             pattern: 'step',
341 |             jobName: extractedJobName,
342 |             limit: 3
343 |           });
344 |           
345 |           assertValidMCPResponse(refinedResult);
346 |           const refinedSearch = assertSearchResultsFormat(refinedResult, 'step', extractedJobName);
347 |           
348 |           // Step 4: Validate refinement worked correctly
349 |           if (refinedSearch.entries.length > 0) {
350 |             for (const entry of refinedSearch.entries) {
351 |               assert.ok(entry.includes(`[${extractedJobName}]`),
352 |                 `Refined search should only return entries from "${extractedJobName}"`);
353 |               assert.ok(entry.toLowerCase().includes('step'),
354 |                 `Refined search should contain "step" pattern`);
355 |             }
356 |           }
357 |         }
358 |       }
359 |     });
360 | 
361 |     test('should handle progressive pattern narrowing', async () => {
362 |       // Progressive narrowing from broad to specific patterns
363 |       const progressivePatterns = ['job', 'step', 'Executing step'];
364 |       const results = [];
365 |       
366 |       for (const pattern of progressivePatterns) {
367 |         const result = await client.callTool('search_job_logs', { 
368 |           pattern: pattern,
369 |           limit: 5
370 |         });
371 |         
372 |         assertValidMCPResponse(result);
373 |         const searchResults = assertSearchResultsFormat(result, pattern);
374 |         results.push({ pattern, matchCount: searchResults.matchCount });
375 |       }
376 |       
377 |       // Each pattern should be valid (no errors)
378 |       assert.equal(results.length, progressivePatterns.length,
379 |         'All progressive patterns should execute successfully');
380 |       
381 |       // More specific patterns should not return more results than broader ones
382 |       if (results[0].matchCount > 0 && results[1].matchCount > 0) {
383 |         assert.ok(results[1].matchCount <= results[0].matchCount,
384 |           'More specific patterns should not exceed broader pattern results');
385 |       }
386 |     });
387 |   });
388 | 
389 |   // === Performance and Reliability Monitoring ===
390 |   describe('Performance and Reliability', () => {
391 |     test('should handle sequential search operations reliably', async () => {
392 |       const searchOperations = [
393 |         { pattern: 'INFO', limit: 3 },
394 |         { pattern: 'step', limit: 2 },
395 |         { pattern: 'completed', limit: 1 }
396 |       ];
397 |       
398 |       const results = [];
399 |       
400 |       // Execute operations sequentially to test reliability
401 |       for (const operation of searchOperations) {
402 |         const result = await client.callTool('search_job_logs', operation);
403 |         
404 |         assertValidMCPResponse(result);
405 |         assert.equal(result.isError, false, 
406 |           `Operation ${JSON.stringify(operation)} should succeed`);
407 |         
408 |         results.push({
409 |           operation,
410 |           success: !result.isError,
411 |           hasContent: result.content && result.content.length > 0
412 |         });
413 |       }
414 |       
415 |       // All operations should succeed
416 |       const successfulOps = results.filter(r => r.success).length;
417 |       assert.equal(successfulOps, searchOperations.length,
418 |         'All sequential operations should succeed');
419 |       
420 |       // All operations should return content
421 |       const opsWithContent = results.filter(r => r.hasContent).length;
422 |       assert.equal(opsWithContent, searchOperations.length,
423 |         'All operations should return content');
424 |     });
425 | 
426 |     test('should provide consistent results for repeated searches', async () => {
427 |       const testPattern = 'INFO';
428 |       const repeatCount = 3;
429 |       const results = [];
430 |       
431 |       // Perform same search multiple times
432 |       for (let i = 0; i < repeatCount; i++) {
433 |         const result = await client.callTool('search_job_logs', { 
434 |           pattern: testPattern,
435 |           limit: 2
436 |         });
437 |         
438 |         assertValidMCPResponse(result);
439 |         assert.equal(result.isError, false, `Iteration ${i + 1} should succeed`);
440 |         
441 |         const text = parseResponseText(result.content[0].text);
442 |         const matchCountMatch = text.match(/Found (\d+) matches/);
443 |         const matchCount = matchCountMatch ? parseInt(matchCountMatch[1]) : 0;
444 |         
445 |         results.push({ iteration: i + 1, matchCount, hasMatches: matchCount > 0 });
446 |       }
447 |       
448 |       // Results should be consistent across iterations
449 |       const uniqueMatchCounts = [...new Set(results.map(r => r.matchCount))];
450 |       assert.equal(uniqueMatchCounts.length, 1,
451 |         `Match counts should be consistent across iterations: ${results.map(r => r.matchCount).join(', ')}`);
452 |     });
453 |   });
454 | 
455 |   // === Comprehensive Error Scenario Testing ===
456 |   describe('Complex Error Scenarios', () => {
457 |     test('should categorize and handle different error types systematically', async () => {
458 |       const errorScenarios = [
459 |         { 
460 |           name: 'validation_error', 
461 |           params: { pattern: '' }, 
462 |           expectedKeywords: ['pattern', 'non-empty', 'string'] 
463 |         },
464 |         { 
465 |           name: 'type_error', 
466 |           params: { pattern: 'INFO', limit: 'invalid' }, 
467 |           expectedKeywords: ['Invalid limit', 'number'] 
468 |         },
469 |         { 
470 |           name: 'constraint_error', 
471 |           params: { pattern: 'INFO', limit: -1 }, 
472 |           expectedKeywords: ['Invalid limit', 'Must be between'] 
473 |         }
474 |       ];
475 |       
476 |       for (const scenario of errorScenarios) {
477 |         const result = await client.callTool('search_job_logs', scenario.params);
478 |         
479 |         assertValidMCPResponse(result);
480 |         assert.equal(result.isError, true, 
481 |           `Scenario "${scenario.name}" should be an error`);
482 |         
483 |         const errorText = result.content[0].text.toLowerCase();
484 |         
485 |         // Check if error contains expected keywords
486 |         const hasExpectedKeywords = scenario.expectedKeywords.some(keyword => 
487 |           errorText.includes(keyword.toLowerCase())
488 |         );
489 |         
490 |         assert.ok(hasExpectedKeywords,
491 |           `Error for "${scenario.name}" should contain keywords: ${scenario.expectedKeywords.join(', ')}. Got: "${result.content[0].text}"`);
492 |       }
493 |     });
494 | 
495 |     test('should handle edge cases with complex validation logic', async () => {
496 |       const edgeCases = [
497 |         { name: 'special_characters', pattern: '[test]', expectSuccess: true },
498 |         { name: 'unicode_characters', pattern: 'тест', expectSuccess: true },
499 |         { name: 'very_long_pattern', pattern: 'a'.repeat(500), expectSuccess: true },
500 |         { name: 'pattern_with_quotes', pattern: '"quoted"', expectSuccess: true }
501 |       ];
502 |       
503 |       for (const edgeCase of edgeCases) {
504 |         const result = await client.callTool('search_job_logs', { 
505 |           pattern: edgeCase.pattern,
506 |           limit: 1
507 |         });
508 |         
509 |         assertValidMCPResponse(result);
510 |         
511 |         if (edgeCase.expectSuccess) {
512 |           assert.equal(result.isError, false,
513 |             `Edge case "${edgeCase.name}" should succeed`);
514 |           
515 |           // Should handle gracefully even if no matches found
516 |           const searchResults = assertSearchResultsFormat(result, edgeCase.pattern);
517 |           assert.ok(searchResults.matchCount >= 0,
518 |             `Edge case "${edgeCase.name}" should return valid match count`);
519 |         } else {
520 |           assert.equal(result.isError, true,
521 |             `Edge case "${edgeCase.name}" should fail as expected`);
522 |         }
523 |       }
524 |     });
525 |   });
526 | });
```

