This is page 25 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?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 -------------------------------------------------------------------------------- /tests/mcp/yaml/get-sfra-document.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | description: "Test get_sfra_document tool for retrieving SFRA documentation" 2 | tests: 3 | # Smoke test - ensure tool is available and accessible 4 | - it: "should be available in tools list" 5 | request: 6 | jsonrpc: "2.0" 7 | id: "list-sfra-document-tool" 8 | method: "tools/list" 9 | params: {} 10 | expect: 11 | response: 12 | jsonrpc: "2.0" 13 | id: "list-sfra-document-tool" 14 | result: 15 | tools: 16 | match:arrayContains:name:get_sfra_document 17 | stderr: "toBeEmpty" 18 | 19 | # Valid document tests - Core SFRA classes 20 | - it: "should retrieve server document successfully" 21 | request: 22 | jsonrpc: "2.0" 23 | id: "get-server-doc" 24 | method: "tools/call" 25 | params: 26 | name: "get_sfra_document" 27 | arguments: 28 | documentName: "server" 29 | expect: 30 | response: 31 | jsonrpc: "2.0" 32 | id: "get-server-doc" 33 | result: 34 | content: 35 | - type: "text" 36 | text: "match:contains:Class Server" 37 | isError: false 38 | performance: 39 | maxResponseTime: "2000ms" 40 | stderr: "toBeEmpty" 41 | 42 | - it: "should retrieve request document successfully" 43 | request: 44 | jsonrpc: "2.0" 45 | id: "get-request-doc" 46 | method: "tools/call" 47 | params: 48 | name: "get_sfra_document" 49 | arguments: 50 | documentName: "request" 51 | expect: 52 | response: 53 | jsonrpc: "2.0" 54 | id: "get-request-doc" 55 | result: 56 | content: 57 | - type: "text" 58 | text: "match:contains:Class Request" 59 | isError: false 60 | performance: 61 | maxResponseTime: "2000ms" 62 | stderr: "toBeEmpty" 63 | 64 | - it: "should retrieve response document successfully" 65 | request: 66 | jsonrpc: "2.0" 67 | id: "get-response-doc" 68 | method: "tools/call" 69 | params: 70 | name: "get_sfra_document" 71 | arguments: 72 | documentName: "response" 73 | expect: 74 | response: 75 | jsonrpc: "2.0" 76 | id: "get-response-doc" 77 | result: 78 | content: 79 | - type: "text" 80 | text: "match:contains:Class Response" 81 | isError: false 82 | performance: 83 | maxResponseTime: "2000ms" 84 | stderr: "toBeEmpty" 85 | 86 | - it: "should retrieve querystring document successfully" 87 | request: 88 | jsonrpc: "2.0" 89 | id: "get-querystring-doc" 90 | method: "tools/call" 91 | params: 92 | name: "get_sfra_document" 93 | arguments: 94 | documentName: "querystring" 95 | expect: 96 | response: 97 | jsonrpc: "2.0" 98 | id: "get-querystring-doc" 99 | result: 100 | content: 101 | - type: "text" 102 | text: "match:contains:QueryString" 103 | isError: false 104 | performance: 105 | maxResponseTime: "2000ms" 106 | stderr: "toBeEmpty" 107 | 108 | - it: "should retrieve render document successfully" 109 | request: 110 | jsonrpc: "2.0" 111 | id: "get-render-doc" 112 | method: "tools/call" 113 | params: 114 | name: "get_sfra_document" 115 | arguments: 116 | documentName: "render" 117 | expect: 118 | response: 119 | jsonrpc: "2.0" 120 | id: "get-render-doc" 121 | result: 122 | content: 123 | - type: "text" 124 | text: "match:contains:render" 125 | isError: false 126 | performance: 127 | maxResponseTime: "2000ms" 128 | stderr: "toBeEmpty" 129 | 130 | # Product model tests 131 | - it: "should retrieve product-full document successfully" 132 | request: 133 | jsonrpc: "2.0" 134 | id: "get-product-full-doc" 135 | method: "tools/call" 136 | params: 137 | name: "get_sfra_document" 138 | arguments: 139 | documentName: "product-full" 140 | expect: 141 | response: 142 | jsonrpc: "2.0" 143 | id: "get-product-full-doc" 144 | result: 145 | content: 146 | - type: "text" 147 | text: "match:contains:product" 148 | isError: false 149 | performance: 150 | maxResponseTime: "2000ms" 151 | stderr: "toBeEmpty" 152 | 153 | - it: "should retrieve cart document successfully" 154 | request: 155 | jsonrpc: "2.0" 156 | id: "get-cart-doc" 157 | method: "tools/call" 158 | params: 159 | name: "get_sfra_document" 160 | arguments: 161 | documentName: "cart" 162 | expect: 163 | response: 164 | jsonrpc: "2.0" 165 | id: "get-cart-doc" 166 | result: 167 | content: 168 | - type: "text" 169 | text: "match:contains:cart" 170 | isError: false 171 | performance: 172 | maxResponseTime: "2000ms" 173 | stderr: "toBeEmpty" 174 | 175 | # Customer model tests 176 | - it: "should retrieve account document successfully" 177 | request: 178 | jsonrpc: "2.0" 179 | id: "get-account-doc" 180 | method: "tools/call" 181 | params: 182 | name: "get_sfra_document" 183 | arguments: 184 | documentName: "account" 185 | expect: 186 | response: 187 | jsonrpc: "2.0" 188 | id: "get-account-doc" 189 | result: 190 | content: 191 | - type: "text" 192 | text: "match:contains:account" 193 | isError: false 194 | performance: 195 | maxResponseTime: "2000ms" 196 | stderr: "toBeEmpty" 197 | 198 | # Response format validation tests 199 | - it: "should return properly structured JSON content" 200 | request: 201 | jsonrpc: "2.0" 202 | id: "validate-server-structure" 203 | method: "tools/call" 204 | params: 205 | name: "get_sfra_document" 206 | arguments: 207 | documentName: "server" 208 | expect: 209 | response: 210 | jsonrpc: "2.0" 211 | id: "validate-server-structure" 212 | result: 213 | content: 214 | - type: "text" 215 | text: "match:regex:\\{[\\s\\S]*title[\\s\\S]*sections[\\s\\S]*content[\\s\\S]*\\}" 216 | isError: false 217 | stderr: "toBeEmpty" 218 | 219 | - it: "should include document metadata" 220 | request: 221 | jsonrpc: "2.0" 222 | id: "validate-metadata" 223 | method: "tools/call" 224 | params: 225 | name: "get_sfra_document" 226 | arguments: 227 | documentName: "server" 228 | expect: 229 | response: 230 | jsonrpc: "2.0" 231 | id: "validate-metadata" 232 | result: 233 | content: 234 | - type: "text" 235 | text: "match:regex:[\\s\\S]*type[\\s\\S]*category[\\s\\S]*filename[\\s\\S]*lastModified[\\s\\S]*" 236 | isError: false 237 | stderr: "toBeEmpty" 238 | 239 | - it: "should include sections array" 240 | request: 241 | jsonrpc: "2.0" 242 | id: "validate-sections" 243 | method: "tools/call" 244 | params: 245 | name: "get_sfra_document" 246 | arguments: 247 | documentName: "server" 248 | expect: 249 | response: 250 | jsonrpc: "2.0" 251 | id: "validate-sections" 252 | result: 253 | content: 254 | - type: "text" 255 | text: "match:regex:[\\s\\S]*sections[\\s\\S]*\\[[\\s\\S]*\\][\\s\\S]*" 256 | isError: false 257 | stderr: "toBeEmpty" 258 | 259 | - it: "should include comprehensive content" 260 | request: 261 | jsonrpc: "2.0" 262 | id: "validate-content-detail" 263 | method: "tools/call" 264 | params: 265 | name: "get_sfra_document" 266 | arguments: 267 | documentName: "server" 268 | expect: 269 | response: 270 | jsonrpc: "2.0" 271 | id: "validate-content-detail" 272 | result: 273 | content: 274 | - type: "text" 275 | text: "match:regex:[\\s\\S]*Description[\\s\\S]*Method Summary[\\s\\S]*Method Detail[\\s\\S]*" 276 | isError: false 277 | stderr: "toBeEmpty" 278 | 279 | # Error handling tests 280 | - it: "should handle nonexistent document gracefully" 281 | request: 282 | jsonrpc: "2.0" 283 | id: "error-nonexistent" 284 | method: "tools/call" 285 | params: 286 | name: "get_sfra_document" 287 | arguments: 288 | documentName: "nonexistent-document" 289 | expect: 290 | response: 291 | jsonrpc: "2.0" 292 | id: "error-nonexistent" 293 | result: 294 | content: 295 | - type: "text" 296 | text: "match:contains:not found" 297 | isError: true 298 | stderr: "toBeEmpty" 299 | 300 | - it: "should handle empty document name" 301 | request: 302 | jsonrpc: "2.0" 303 | id: "error-empty-name" 304 | method: "tools/call" 305 | params: 306 | name: "get_sfra_document" 307 | arguments: 308 | documentName: "" 309 | expect: 310 | response: 311 | jsonrpc: "2.0" 312 | id: "error-empty-name" 313 | result: 314 | content: 315 | - type: "text" 316 | text: "match:regex:(Error|not found|invalid)" 317 | isError: true 318 | stderr: "toBeEmpty" 319 | 320 | - it: "should handle invalid document name characters" 321 | request: 322 | jsonrpc: "2.0" 323 | id: "error-invalid-chars" 324 | method: "tools/call" 325 | params: 326 | name: "get_sfra_document" 327 | arguments: 328 | documentName: "invalid/document/name" 329 | expect: 330 | response: 331 | jsonrpc: "2.0" 332 | id: "error-invalid-chars" 333 | result: 334 | content: 335 | - type: "text" 336 | text: "match:regex:(Error|not found|invalid)" 337 | isError: true 338 | stderr: "toBeEmpty" 339 | 340 | - it: "should handle missing documentName parameter" 341 | request: 342 | jsonrpc: "2.0" 343 | id: "error-missing-param" 344 | method: "tools/call" 345 | params: 346 | name: "get_sfra_document" 347 | arguments: {} 348 | expect: 349 | response: 350 | jsonrpc: "2.0" 351 | id: "error-missing-param" 352 | result: 353 | content: 354 | - type: "text" 355 | text: "match:regex:(Error|required|missing|documentName|non-empty string)" 356 | isError: true 357 | stderr: "toBeEmpty" 358 | 359 | # Case sensitivity tests - it appears the tool is case-insensitive 360 | - it: "should handle case variations for document names" 361 | request: 362 | jsonrpc: "2.0" 363 | id: "case-insensitive-upper" 364 | method: "tools/call" 365 | params: 366 | name: "get_sfra_document" 367 | arguments: 368 | documentName: "SERVER" 369 | expect: 370 | response: 371 | jsonrpc: "2.0" 372 | id: "case-insensitive-upper" 373 | result: 374 | content: 375 | - type: "text" 376 | text: "match:contains:Class Server" 377 | isError: false 378 | stderr: "toBeEmpty" 379 | 380 | - it: "should handle mixed case document names" 381 | request: 382 | jsonrpc: "2.0" 383 | id: "case-insensitive-mixed" 384 | method: "tools/call" 385 | params: 386 | name: "get_sfra_document" 387 | arguments: 388 | documentName: "Server" 389 | expect: 390 | response: 391 | jsonrpc: "2.0" 392 | id: "case-insensitive-mixed" 393 | result: 394 | content: 395 | - type: "text" 396 | text: "match:contains:Class Server" 397 | isError: false 398 | stderr: "toBeEmpty" 399 | 400 | # Content quality tests 401 | - it: "should provide meaningful content for server document" 402 | request: 403 | jsonrpc: "2.0" 404 | id: "content-quality-server" 405 | method: "tools/call" 406 | params: 407 | name: "get_sfra_document" 408 | arguments: 409 | documentName: "server" 410 | expect: 411 | response: 412 | jsonrpc: "2.0" 413 | id: "content-quality-server" 414 | result: 415 | content: 416 | - type: "text" 417 | text: "match:regex:[\\s\\S]*middleware[\\s\\S]*routing[\\s\\S]*HTTP[\\s\\S]*" 418 | isError: false 419 | stderr: "toBeEmpty" 420 | 421 | - it: "should provide meaningful content for request document" 422 | request: 423 | jsonrpc: "2.0" 424 | id: "content-quality-request" 425 | method: "tools/call" 426 | params: 427 | name: "get_sfra_document" 428 | arguments: 429 | documentName: "request" 430 | expect: 431 | response: 432 | jsonrpc: "2.0" 433 | id: "content-quality-request" 434 | result: 435 | content: 436 | - type: "text" 437 | text: "match:regex:[\\s\\S]*HTTP[\\s\\S]*session[\\s\\S]*customer[\\s\\S]*" 438 | isError: false 439 | stderr: "toBeEmpty" 440 | 441 | # Additional model document tests 442 | - it: "should retrieve billing document successfully" 443 | request: 444 | jsonrpc: "2.0" 445 | id: "get-billing-doc" 446 | method: "tools/call" 447 | params: 448 | name: "get_sfra_document" 449 | arguments: 450 | documentName: "billing" 451 | expect: 452 | response: 453 | jsonrpc: "2.0" 454 | id: "get-billing-doc" 455 | result: 456 | content: 457 | - type: "text" 458 | text: "match:contains:billing" 459 | isError: false 460 | performance: 461 | maxResponseTime: "2000ms" 462 | stderr: "toBeEmpty" 463 | 464 | - it: "should retrieve shipping document successfully" 465 | request: 466 | jsonrpc: "2.0" 467 | id: "get-shipping-doc" 468 | method: "tools/call" 469 | params: 470 | name: "get_sfra_document" 471 | arguments: 472 | documentName: "shipping" 473 | expect: 474 | response: 475 | jsonrpc: "2.0" 476 | id: "get-shipping-doc" 477 | result: 478 | content: 479 | - type: "text" 480 | text: "match:contains:shipping" 481 | isError: false 482 | performance: 483 | maxResponseTime: "2000ms" 484 | stderr: "toBeEmpty" 485 | 486 | # Edge case tests 487 | - it: "should handle document names with hyphens" 488 | request: 489 | jsonrpc: "2.0" 490 | id: "hyphen-document" 491 | method: "tools/call" 492 | params: 493 | name: "get_sfra_document" 494 | arguments: 495 | documentName: "product-full" 496 | expect: 497 | response: 498 | jsonrpc: "2.0" 499 | id: "hyphen-document" 500 | result: 501 | content: 502 | - type: "text" 503 | text: "match:type:string" 504 | isError: false 505 | stderr: "toBeEmpty" 506 | 507 | - it: "should handle very long nonexistent document names" 508 | request: 509 | jsonrpc: "2.0" 510 | id: "long-nonexistent" 511 | method: "tools/call" 512 | params: 513 | name: "get_sfra_document" 514 | arguments: 515 | documentName: "this-is-a-very-long-document-name-that-definitely-does-not-exist-anywhere" 516 | expect: 517 | response: 518 | jsonrpc: "2.0" 519 | id: "long-nonexistent" 520 | result: 521 | content: 522 | - type: "text" 523 | text: "match:regex:(Error|not found)" 524 | isError: true 525 | stderr: "toBeEmpty" 526 | ``` -------------------------------------------------------------------------------- /tests/mcp/node/get-log-file-contents.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('get_log_file_contents - Optimized Programmatic Tests', () => { 6 | let client; 7 | let testFile; 8 | 9 | before(async () => { 10 | client = await connect('./aegis.config.with-dw.json'); 11 | 12 | // Discover one working test file for optimization 13 | testFile = await discoverTestFile(); 14 | }); 15 | 16 | after(async () => { 17 | if (client?.connected) { 18 | await client.disconnect(); 19 | } 20 | }); 21 | 22 | beforeEach(() => { 23 | // CRITICAL: Clear all buffers to prevent leaking into next tests 24 | client.clearAllBuffers(); 25 | }); 26 | 27 | // === Helper Functions === 28 | function assertValidMCPResponse(result) { 29 | assert.ok(result.content, 'Should have content'); 30 | assert.ok(Array.isArray(result.content), 'Content should be array'); 31 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); 32 | } 33 | 34 | function parseResponseText(text) { 35 | return text.startsWith('"') && text.endsWith('"') ? JSON.parse(text) : text; 36 | } 37 | 38 | function assertTextContent(result, expectedSubstring) { 39 | assertValidMCPResponse(result); 40 | assert.equal(result.content[0].type, 'text'); 41 | const actualText = parseResponseText(result.content[0].text); 42 | assert.ok(actualText.includes(expectedSubstring), 43 | `Expected "${expectedSubstring}" in response`); 44 | } 45 | 46 | function assertSuccessResponse(result) { 47 | assertValidMCPResponse(result); 48 | assert.equal(result.isError, false, 'Should not be an error response'); 49 | assert.equal(result.content[0].type, 'text'); 50 | } 51 | 52 | function assertErrorResponse(result, expectedErrorSubstring) { 53 | assertValidMCPResponse(result); 54 | assert.equal(result.isError, true, 'Should be an error response'); 55 | if (expectedErrorSubstring) { 56 | assertTextContent(result, expectedErrorSubstring); 57 | } 58 | } 59 | 60 | async function discoverTestFile() { 61 | // Try to discover one working file using the job log tool 62 | try { 63 | const result = await client.callTool('get_latest_job_log_files', {}); 64 | if (!result.isError) { 65 | const text = parseResponseText(result.content[0].text); 66 | const match = text.match(/File:\s+(.+\.log)/); 67 | if (match) { 68 | return `jobs/ImportCatalog/${match[1]}`; 69 | } 70 | } 71 | } catch { 72 | // Ignore discovery errors 73 | } 74 | 75 | // Fallback to expected file 76 | return 'jobs/ImportCatalog/Job-ImportCatalog-0987654321.log'; 77 | } 78 | 79 | // === Core Functionality Tests === 80 | describe('Core Functionality', () => { 81 | test('should read log file with complete metadata', async () => { 82 | const result = await client.callTool('get_log_file_contents', { 83 | filename: testFile 84 | }); 85 | 86 | assertSuccessResponse(result); 87 | assertTextContent(result, 'Log File Contents:'); 88 | assertTextContent(result, testFile); 89 | assertTextContent(result, 'Total lines:'); 90 | assertTextContent(result, 'Content size:'); 91 | 92 | // Validate SFCC timestamp pattern 93 | const content = parseResponseText(result.content[0].text); 94 | const timestampPattern = /\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT\]/; 95 | assert.ok(timestampPattern.test(content), 'Should contain SFCC timestamps'); 96 | }); 97 | 98 | test('should handle maxBytes parameter with content validation', async () => { 99 | const result = await client.callTool('get_log_file_contents', { 100 | filename: testFile, 101 | maxBytes: 500 102 | }); 103 | 104 | assertSuccessResponse(result); 105 | assertTextContent(result, 'Content size: 500 bytes'); 106 | 107 | // Should still include essential metadata despite size limit 108 | assertTextContent(result, 'Log File Contents:'); 109 | assertTextContent(result, 'Total lines:'); 110 | }); 111 | 112 | test('should handle tailOnly parameter correctly', async () => { 113 | const fullResult = await client.callTool('get_log_file_contents', { 114 | filename: testFile, 115 | maxBytes: 1000 116 | }); 117 | 118 | const tailResult = await client.callTool('get_log_file_contents', { 119 | filename: testFile, 120 | maxBytes: 1000, 121 | tailOnly: true 122 | }); 123 | 124 | assertSuccessResponse(fullResult); 125 | assertSuccessResponse(tailResult); 126 | 127 | assertTextContent(tailResult, 'tail read'); 128 | assertTextContent(tailResult, 'Content size: 1000 bytes'); 129 | }); 130 | }); 131 | 132 | // === Dynamic Validation Tests === 133 | describe('Dynamic Validation', () => { 134 | test('should validate content structure consistency', async () => { 135 | const testCases = [ 136 | { maxBytes: 200 }, 137 | { maxBytes: 1000 }, 138 | { tailOnly: true, maxBytes: 500 }, 139 | { tailOnly: false } 140 | ]; 141 | 142 | for (const params of testCases) { 143 | const result = await client.callTool('get_log_file_contents', { 144 | filename: testFile, 145 | ...params 146 | }); 147 | 148 | assertSuccessResponse(result); 149 | 150 | // All responses should have consistent structure 151 | assert.equal(result.content.length, 1, 'Should have one content element'); 152 | 153 | const content = parseResponseText(result.content[0].text); 154 | assert.ok(content.includes('Log File Contents:'), 'Should have header'); 155 | assert.ok(content.includes('Total lines:'), 'Should have line count'); 156 | } 157 | }); 158 | 159 | test('should handle parameter edge cases with validation', async () => { 160 | const edgeCases = [ 161 | { name: 'minimal bytes', params: { maxBytes: 1 } }, 162 | { name: 'large bytes', params: { maxBytes: 50000 } }, 163 | { name: 'tail with small bytes', params: { tailOnly: true, maxBytes: 10 } } 164 | ]; 165 | 166 | for (const testCase of edgeCases) { 167 | const result = await client.callTool('get_log_file_contents', { 168 | filename: testFile, 169 | ...testCase.params 170 | }); 171 | 172 | assertSuccessResponse(result); 173 | 174 | const content = parseResponseText(result.content[0].text); 175 | 176 | // Even with edge cases, should maintain structure 177 | assert.ok(content.includes('Log File Contents:'), 178 | `${testCase.name} should maintain header structure`); 179 | } 180 | }); 181 | }); 182 | 183 | // === Error Handling Tests === 184 | describe('Error Handling', () => { 185 | test('should handle invalid parameters gracefully', async () => { 186 | const errorCases = [ 187 | { args: {}, expectedError: 'filename' }, 188 | { args: { filename: '' }, expectedError: 'filename' }, 189 | { args: { filename: testFile, maxBytes: 0 }, expectedError: 'Invalid maxBytes' }, 190 | { args: { filename: testFile, maxBytes: -1 }, expectedError: 'Invalid maxBytes' } 191 | ]; 192 | 193 | for (const errorCase of errorCases) { 194 | const result = await client.callTool('get_log_file_contents', errorCase.args); 195 | 196 | assertErrorResponse(result, errorCase.expectedError); 197 | } 198 | }); 199 | 200 | test('should handle security-related filename patterns', async () => { 201 | const securityTests = [ 202 | '../../../etc/passwd', 203 | '/etc/passwd', 204 | 'file\x00.log', 205 | 'file|rm -rf /.log' 206 | ]; 207 | 208 | for (const filename of securityTests) { 209 | const result = await client.callTool('get_log_file_contents', { filename }); 210 | 211 | assertValidMCPResponse(result); 212 | 213 | // Should not return sensitive system content 214 | if (!result.isError) { 215 | const content = parseResponseText(result.content[0].text); 216 | assert.ok(!content.includes('root:x:0:0'), 217 | 'Should not return system password file content'); 218 | } 219 | } 220 | }); 221 | }); 222 | 223 | // === Multi-Step Integration Tests === 224 | describe('Integration Workflows', () => { 225 | test('should integrate with file discovery tools', async () => { 226 | // Step 1: Discover available files 227 | const listResult = await client.callTool('get_latest_job_log_files', {}); 228 | 229 | if (!listResult.isError) { 230 | const listText = parseResponseText(listResult.content[0].text); 231 | 232 | // Step 2: Extract a file path from the list 233 | const fileMatch = listText.match(/File:\s+(.+\.log)/); 234 | 235 | if (fileMatch) { 236 | const discoveredFile = `jobs/ImportCatalog/${fileMatch[1]}`; 237 | 238 | // Step 3: Read the discovered file 239 | const contentResult = await client.callTool('get_log_file_contents', { 240 | filename: discoveredFile 241 | }); 242 | 243 | assertSuccessResponse(contentResult); 244 | assertTextContent(contentResult, discoveredFile); 245 | } 246 | } 247 | }); 248 | 249 | test('should support progressive content analysis workflow', async () => { 250 | // Step 1: Get a sample of log content 251 | const sampleResult = await client.callTool('get_log_file_contents', { 252 | filename: testFile, 253 | maxBytes: 1000 254 | }); 255 | 256 | assertSuccessResponse(sampleResult); 257 | const sampleContent = parseResponseText(sampleResult.content[0].text); 258 | 259 | // Step 2: Based on sample, get full content if needed 260 | if (sampleContent.includes('INFO')) { 261 | const fullResult = await client.callTool('get_log_file_contents', { 262 | filename: testFile, 263 | maxBytes: 10000 264 | }); 265 | 266 | assertSuccessResponse(fullResult); 267 | const fullContent = parseResponseText(fullResult.content[0].text); 268 | 269 | // Step 3: Validate that full content contains sample content structure 270 | assert.ok(fullContent.includes('Log File Contents:'), 'Full content should have header'); 271 | assert.ok(fullContent.length >= sampleContent.length, 'Full content should be larger'); 272 | } 273 | }); 274 | }); 275 | 276 | // === Business Logic Validation === 277 | describe('Business Logic Validation', () => { 278 | test('should validate log entry format and content', async () => { 279 | const result = await client.callTool('get_log_file_contents', { 280 | filename: testFile, 281 | maxBytes: 2000 282 | }); 283 | 284 | assertSuccessResponse(result); 285 | const content = parseResponseText(result.content[0].text); 286 | 287 | // Validate SFCC-specific log structure 288 | const logEntryPattern = /\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT)\]\s+(INFO|WARN|ERROR|DEBUG)\s+(\w+)/; 289 | const matches = content.match(logEntryPattern); 290 | 291 | if (matches) { 292 | // Validate timestamp is recent - Fix timestamp parsing for SFCC format 293 | // Convert "2025-09-22 12:00:00.000 GMT" to "2025-09-22T12:00:00.000Z" 294 | const isoTimestamp = matches[1].replace(' ', 'T').replace(' GMT', 'Z'); 295 | const logDate = new Date(isoTimestamp); 296 | const now = new Date(); 297 | const daysDiff = (now - logDate) / (1000 * 60 * 60 * 24); 298 | 299 | // Allow reasonable timeframe for test log data (within 1 day of current time) 300 | assert.ok(Math.abs(daysDiff) < 1, 'Log timestamp should be within reasonable timeframe (±1 day)'); 301 | 302 | // Validate log level 303 | assert.ok(['INFO', 'WARN', 'ERROR', 'DEBUG'].includes(matches[2]), 'Should have valid log level'); 304 | 305 | // Validate thread/component information 306 | assert.ok(matches[3].length > 0, 'Should have thread/component information'); 307 | } 308 | }); 309 | 310 | test('should validate content size reporting accuracy', async () => { 311 | const testSizes = [100, 500, 1000]; 312 | 313 | for (const size of testSizes) { 314 | const result = await client.callTool('get_log_file_contents', { 315 | filename: testFile, 316 | maxBytes: size 317 | }); 318 | 319 | assertSuccessResponse(result); 320 | assertTextContent(result, `Content size: ${size} bytes`); 321 | 322 | // Validate that actual content respects the size limit 323 | const content = parseResponseText(result.content[0].text); 324 | const actualContentMatch = content.match(/---\n\n([\s\S]*)$/); 325 | 326 | if (actualContentMatch) { 327 | const actualLogContent = actualContentMatch[1]; 328 | // Should be approximately the requested size (allowing for encoding differences) 329 | assert.ok(actualLogContent.length <= size + 50, 330 | `Content should respect size limit: ${actualLogContent.length} <= ${size + 50}`); 331 | } 332 | } 333 | }); 334 | }); 335 | 336 | // === Reliability and State Management === 337 | describe('Reliability Testing', () => { 338 | test('should maintain state consistency across requests', async () => { 339 | const params = { filename: testFile, maxBytes: 1000 }; 340 | const results = []; 341 | 342 | // Make multiple identical requests 343 | for (let i = 0; i < 3; i++) { 344 | const result = await client.callTool('get_log_file_contents', params); 345 | results.push(result); 346 | assertSuccessResponse(result); 347 | } 348 | 349 | // All results should be identical (no state leakage) 350 | const firstContent = parseResponseText(results[0].content[0].text); 351 | for (let i = 1; i < results.length; i++) { 352 | const content = parseResponseText(results[i].content[0].text); 353 | assert.equal(content, firstContent, `Request ${i} should match first request`); 354 | } 355 | }); 356 | 357 | test('should handle alternating parameter patterns', async () => { 358 | const patterns = [ 359 | { maxBytes: 500 }, 360 | { tailOnly: true, maxBytes: 300 }, 361 | { maxBytes: 1000 }, 362 | { tailOnly: false } 363 | ]; 364 | 365 | for (let i = 0; i < patterns.length; i++) { 366 | const result = await client.callTool('get_log_file_contents', { 367 | filename: testFile, 368 | ...patterns[i] 369 | }); 370 | 371 | assertSuccessResponse(result); 372 | 373 | // Each request should succeed independently 374 | const content = parseResponseText(result.content[0].text); 375 | assert.ok(content.includes('Log File Contents:'), 376 | `Pattern ${i} should maintain proper structure`); 377 | } 378 | }); 379 | }); 380 | }); 381 | ``` -------------------------------------------------------------------------------- /docs/dw_util/SecureFilter.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.util 2 | 3 | # Class SecureFilter 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.util.SecureFilter 9 | 10 | ## Description 11 | 12 | SecureFilter contains many methods for manipulating untrusted data Strings into RFC-Compliant Strings for a given context by removing "bad" data from the untrusted data. 13 | 14 | ## Constructor Summary 15 | 16 | ## Method Summary 17 | 18 | ### forHtmlContent 19 | 20 | **Signature:** `static forHtmlContent(input : String) : String` 21 | 22 | Filters illegal characters from a given input for use in a general HTML context. 23 | 24 | ### forHtmlInDoubleQuoteAttribute 25 | 26 | **Signature:** `static forHtmlInDoubleQuoteAttribute(input : String) : String` 27 | 28 | Filters illegal characters from a given input for use in an HTML Attribute guarded by a double quote. 29 | 30 | ### forHtmlInSingleQuoteAttribute 31 | 32 | **Signature:** `static forHtmlInSingleQuoteAttribute(input : String) : String` 33 | 34 | Filters illegal characters from a given input for use in an HTML Attribute guarded by a single quote. 35 | 36 | ### forHtmlUnquotedAttribute 37 | 38 | **Signature:** `static forHtmlUnquotedAttribute(input : String) : String` 39 | 40 | Filters illegal characters from a given input for use in an HTML Attribute left unguarded. 41 | 42 | ### forJavaScriptInAttribute 43 | 44 | **Signature:** `static forJavaScriptInAttribute(input : String) : String` 45 | 46 | Filters illegal characters from a given input for use in JavaScript inside an HTML attribute. 47 | 48 | ### forJavaScriptInBlock 49 | 50 | **Signature:** `static forJavaScriptInBlock(input : String) : String` 51 | 52 | Filters illegal characters from a given input for use in JavaScript inside an HTML block. 53 | 54 | ### forJavaScriptInHTML 55 | 56 | **Signature:** `static forJavaScriptInHTML(input : String) : String` 57 | 58 | Filters illegal characters from a given input for use in JavaScript inside an HTML context. 59 | 60 | ### forJavaScriptInSource 61 | 62 | **Signature:** `static forJavaScriptInSource(input : String) : String` 63 | 64 | Filters illegal characters from a given input for use in JavaScript inside a JavaScript source file. 65 | 66 | ### forJSONValue 67 | 68 | **Signature:** `static forJSONValue(input : String) : String` 69 | 70 | Filters illegal characters from a given input for use in a JSON Object Value to prevent escaping into a trusted context. 71 | 72 | ### forUriComponent 73 | 74 | **Signature:** `static forUriComponent(input : String) : String` 75 | 76 | Filters illegal characters from a given input for use as a component of a URI. 77 | 78 | ### forUriComponentStrict 79 | 80 | **Signature:** `static forUriComponentStrict(input : String) : String` 81 | 82 | Filters illegal characters from a given input for use as a component of a URI. 83 | 84 | ### forXmlCommentContent 85 | 86 | **Signature:** `static forXmlCommentContent(input : String) : String` 87 | 88 | Filters illegal characters from a given input for use in an XML comments. 89 | 90 | ### forXmlContent 91 | 92 | **Signature:** `static forXmlContent(input : String) : String` 93 | 94 | Filters illegal characters from a given input for use in a general XML context. 95 | 96 | ### forXmlInDoubleQuoteAttribute 97 | 98 | **Signature:** `static forXmlInDoubleQuoteAttribute(input : String) : String` 99 | 100 | Filters illegal characters from a given input for use in an XML attribute guarded by a double quote. 101 | 102 | ### forXmlInSingleQuoteAttribute 103 | 104 | **Signature:** `static forXmlInSingleQuoteAttribute(input : String) : String` 105 | 106 | Filters illegal characters from a given input for use in an XML attribute guarded by a single quote. 107 | 108 | ## Method Detail 109 | 110 | ## Method Details 111 | 112 | ### forHtmlContent 113 | 114 | **Signature:** `static forHtmlContent(input : String) : String` 115 | 116 | **Description:** Filters illegal characters from a given input for use in a general HTML context. E.g. text content and text attributes. This method takes the UNION of allowed characters among all contexts, so may be more imprecise that the more specific contexts. Generally, this method is preferred unless you specifically understand the context in which untrusted data will be output. Example Usage: <div>${SecureFilter.forHtmlContent(unsafeData)}</div> <input value="${SecureFilter.forHtmlContent(unsafeData)}" /> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 117 | 118 | **Parameters:** 119 | 120 | - `input`: untrusted input to be filtered, if necessary 121 | 122 | **Returns:** 123 | 124 | a properly filtered string for the given input 125 | 126 | --- 127 | 128 | ### forHtmlInDoubleQuoteAttribute 129 | 130 | **Signature:** `static forHtmlInDoubleQuoteAttribute(input : String) : String` 131 | 132 | **Description:** Filters illegal characters from a given input for use in an HTML Attribute guarded by a double quote. This method is preferred if you understand exactly how the output of this will be used in the HTML document. Example Usage: <div id="${SecureFilter.forHtmlInDoubleQuoteAttribute(unsafeData)}"></div> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 133 | 134 | **Parameters:** 135 | 136 | - `input`: untrusted input to be filtered, if necessary 137 | 138 | **Returns:** 139 | 140 | a properly filtered string for the given input 141 | 142 | --- 143 | 144 | ### forHtmlInSingleQuoteAttribute 145 | 146 | **Signature:** `static forHtmlInSingleQuoteAttribute(input : String) : String` 147 | 148 | **Description:** Filters illegal characters from a given input for use in an HTML Attribute guarded by a single quote. This method is preferred if you understand exactly how the output of this will be used in the HTML document. Example Usage: <div id='${SecureFilter.forHtmlInSingleQuoteAttribute(unsafeData)}'></div> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 149 | 150 | **Parameters:** 151 | 152 | - `input`: untrusted input to be filterd, if necessary 153 | 154 | **Returns:** 155 | 156 | a properly filtered string for the given input 157 | 158 | --- 159 | 160 | ### forHtmlUnquotedAttribute 161 | 162 | **Signature:** `static forHtmlUnquotedAttribute(input : String) : String` 163 | 164 | **Description:** Filters illegal characters from a given input for use in an HTML Attribute left unguarded. This method is preferred if you understand exactly how the output of this will be used in the HTML document. Example Usage: <div id=${SecureFilter.forHtmlUnquotedAttribute(unsafeData)}></div> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 165 | 166 | **Parameters:** 167 | 168 | - `input`: untrusted input to be filtered, if necessary 169 | 170 | **Returns:** 171 | 172 | a properly filtered string for the given input 173 | 174 | --- 175 | 176 | ### forJavaScriptInAttribute 177 | 178 | **Signature:** `static forJavaScriptInAttribute(input : String) : String` 179 | 180 | **Description:** Filters illegal characters from a given input for use in JavaScript inside an HTML attribute. This method is preferred if you understand exactly how the output of the will be used in the page Example Usage: <button onclick="alert('${SecureFilter.forJavaScriptInAttribute(unsafeData)}');"> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 181 | 182 | **Parameters:** 183 | 184 | - `input`: untrusted input to be filtered, if necessary 185 | 186 | **Returns:** 187 | 188 | a properly filtered string for the given input 189 | 190 | --- 191 | 192 | ### forJavaScriptInBlock 193 | 194 | **Signature:** `static forJavaScriptInBlock(input : String) : String` 195 | 196 | **Description:** Filters illegal characters from a given input for use in JavaScript inside an HTML block. This method is preferred if you understand exactly how the output of the will be used in the page Example Usage: <script type="text/javascript"> var data = "${SecureFilter.forJavaScriptInBlock(unsafeData)}"; </script> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 197 | 198 | **Parameters:** 199 | 200 | - `input`: untrusted input to be filtered, if necessary 201 | 202 | **Returns:** 203 | 204 | a properly filtered string for the given input 205 | 206 | --- 207 | 208 | ### forJavaScriptInHTML 209 | 210 | **Signature:** `static forJavaScriptInHTML(input : String) : String` 211 | 212 | **Description:** Filters illegal characters from a given input for use in JavaScript inside an HTML context. This method takes the UNION of allowed characters among the other contexts, so may be more imprecise that the more specific contexts. Generally, this method is preferred unless you specifically understand the context in which untrusted data will be output. Example Usage: <script type="text/javascript"> var data = "${SecureFilter.forJavaScriptInHTML(unsafeData)}"; </script> <button onclick="alert('${SecureFilter.forJavaScriptInHTML(unsafeData)}');"> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 213 | 214 | **Parameters:** 215 | 216 | - `input`: untrusted input to be filtered, if necessary 217 | 218 | **Returns:** 219 | 220 | a properly filtered string for the given input 221 | 222 | --- 223 | 224 | ### forJavaScriptInSource 225 | 226 | **Signature:** `static forJavaScriptInSource(input : String) : String` 227 | 228 | **Description:** Filters illegal characters from a given input for use in JavaScript inside a JavaScript source file. This method is preferred if you understand exactly how the output of the will be used in the page Example Usage: <...inside foobar.js...> var data = "${SecureFilter.forJavaScriptInSource(unsafeData)}"; Flow: Allow AlphaNumerics and some Special characters Remove all other characters 229 | 230 | **Parameters:** 231 | 232 | - `input`: untrusted input to be filtered, if necessary 233 | 234 | **Returns:** 235 | 236 | a properly filtered string for the given input 237 | 238 | --- 239 | 240 | ### forJSONValue 241 | 242 | **Signature:** `static forJSONValue(input : String) : String` 243 | 244 | **Description:** Filters illegal characters from a given input for use in a JSON Object Value to prevent escaping into a trusted context. Example Usage: var json = {"trusted_data" : SecureFilter.forJSONValue(unsafeData)}; return JSON.stringify(json); Flow: Allow AlphaNumerics Remove all other characters 245 | 246 | **Parameters:** 247 | 248 | - `input`: ed input to be filtered, if necessary 249 | 250 | **Returns:** 251 | 252 | a properly filtered string for the given input 253 | 254 | --- 255 | 256 | ### forUriComponent 257 | 258 | **Signature:** `static forUriComponent(input : String) : String` 259 | 260 | **Description:** Filters illegal characters from a given input for use as a component of a URI. This is equivalent to javascript's filterURIComponent and does a realistic job of encoding. Example Usage: <a href="http://host.com?value=${SecureFilter.forUriComponent(unsafeData)}"/> Allows: A-Z, a-z, 0-9, -, _, ., ~, !, *, ', (, ) Flow: Allow AlphaNumerics and some Special characters Remove all other characters 261 | 262 | **Parameters:** 263 | 264 | - `input`: untrusted input to be filtered, if necessary 265 | 266 | **Returns:** 267 | 268 | a properly filtered string for the given input 269 | 270 | --- 271 | 272 | ### forUriComponentStrict 273 | 274 | **Signature:** `static forUriComponentStrict(input : String) : String` 275 | 276 | **Description:** Filters illegal characters from a given input for use as a component of a URI. This is a strict filter and fully complies with RFC3986. Example Usage: <a href="http://host.com?value=${SecureFilter.forUriComponentStrict(unsafeData)}"/> Allows: A-Z, a-z, 0-9, -, _, ., ~ Flow: Allow AlphaNumerics and some Special characters Remove all other characters 277 | 278 | **Parameters:** 279 | 280 | - `input`: untrusted input to be filtered, if necessary 281 | 282 | **Returns:** 283 | 284 | a properly filtered string for the given input 285 | 286 | --- 287 | 288 | ### forXmlCommentContent 289 | 290 | **Signature:** `static forXmlCommentContent(input : String) : String` 291 | 292 | **Description:** Filters illegal characters from a given input for use in an XML comments. This method is preferred if you understand the context in which untrusted data will be output. Note: It is recommended that you use a real parser, as this method can be misused, but is left here if a parser is unavailable to you Example Usage: <!-- ${SecureFilter.forXmlCommentContent(unsafeData)} --> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 293 | 294 | **Parameters:** 295 | 296 | - `input`: untrusted input to be filtered, if necessary 297 | 298 | **Returns:** 299 | 300 | a properly filtered string for the given input 301 | 302 | --- 303 | 304 | ### forXmlContent 305 | 306 | **Signature:** `static forXmlContent(input : String) : String` 307 | 308 | **Description:** Filters illegal characters from a given input for use in a general XML context. E.g. text content and text attributes. This method takes the UNION of allowed characters between the other contexts, so may be more imprecise that the more specific contexts. Generally, this method is preferred unless you specifically understand the context in which untrusted data will be output. Note: It is recommended that you use a real parser, as this method can be misused, but is left here if a parser is unavailable to you Example Usage: <foo>${SecureFilter.forXmlContent(unsafeData)}</foo> <bar attr="${SecureFilter.forXmlContent(unsafeData)}"></bar> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 309 | 310 | **Parameters:** 311 | 312 | - `input`: untrusted input to be filtered, if necessary 313 | 314 | **Returns:** 315 | 316 | a properly filtered string for the given input 317 | 318 | --- 319 | 320 | ### forXmlInDoubleQuoteAttribute 321 | 322 | **Signature:** `static forXmlInDoubleQuoteAttribute(input : String) : String` 323 | 324 | **Description:** Filters illegal characters from a given input for use in an XML attribute guarded by a double quote. This method is preferred if you understand the context in which untrusted data will be output. Note: It is recommended that you use a real parser, as this method can be misused, but is left here if a parser is unavailable to you Example Usage: <bar attr="${SecureFilter.forXmlInDoubleQuoteAttribute(unsafeData)}"></bar> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 325 | 326 | **Parameters:** 327 | 328 | - `input`: untrusted input to be filtered, if necessary 329 | 330 | **Returns:** 331 | 332 | a properly filtered string for the given input 333 | 334 | --- 335 | 336 | ### forXmlInSingleQuoteAttribute 337 | 338 | **Signature:** `static forXmlInSingleQuoteAttribute(input : String) : String` 339 | 340 | **Description:** Filters illegal characters from a given input for use in an XML attribute guarded by a single quote. This method is preferred if you understand the context in which untrusted data will be output. Note: It is recommended that you use a real parser, as this method can be misused, but is left here if a parser is unavailable to you Example Usage: <bar attr='${SecureFilter.forXmlInSingleQuoteAttribute(unsafeData)}'></bar> Flow: Allow AlphaNumerics and some Special characters Remove all other characters 341 | 342 | **Parameters:** 343 | 344 | - `input`: untrusted input to be filtered, if necessary 345 | 346 | **Returns:** 347 | 348 | a properly filtered string for the given input 349 | 350 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/node/get-latest-debug.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 | // Optimized programmatic tests focusing on complex business logic, dynamic validation, and integration scenarios 6 | // Basic functional testing is handled by the YAML tests for better CI reliability 7 | describe('get_latest_debug - Full Mode Programmatic Tests (Optimized)', () => { 8 | let client; 9 | 10 | before(async () => { 11 | client = await connect('./aegis.config.with-dw.json'); 12 | }); 13 | 14 | after(async () => { 15 | if (client?.connected) { 16 | await client.disconnect(); 17 | } 18 | }); 19 | 20 | beforeEach(() => { 21 | // CRITICAL: Clear all buffers to prevent leaking into next tests 22 | client.clearAllBuffers(); // Recommended - comprehensive protection 23 | }); 24 | 25 | // Enhanced helper functions for complex validations 26 | function assertValidMCPResponse(result) { 27 | assert.ok(result.content, 'Should have content'); 28 | assert.ok(Array.isArray(result.content), 'Content should be array'); 29 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); 30 | } 31 | 32 | 33 | 34 | function assertSuccessResponse(result) { 35 | assertValidMCPResponse(result); 36 | assert.equal(result.isError, false, 'Should not be an error response'); 37 | assert.equal(result.content[0].type, 'text'); 38 | } 39 | 40 | function validateDebugLogStructure(result, expectedLimit) { 41 | assertSuccessResponse(result); 42 | const text = result.content[0].text; 43 | 44 | // Validate essential debug log components 45 | assert.ok(text.includes(`Latest ${expectedLimit} debug messages`), 46 | `Should mention "${expectedLimit}" debug messages`); 47 | assert.ok(/debug-blade-\d{8}-\d{6}\.log/.test(text), 48 | 'Should contain debug log file name pattern'); 49 | assert.ok(text.includes('DEBUG'), 'Should contain DEBUG level entries'); 50 | assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(text), 51 | 'Should contain GMT timestamp pattern'); 52 | } 53 | 54 | function getCurrentDateString() { 55 | const now = new Date(); 56 | const year = now.getFullYear(); 57 | const month = String(now.getMonth() + 1).padStart(2, '0'); 58 | const day = String(now.getDate()).padStart(2, '0'); 59 | return `${year}${month}${day}`; 60 | } 61 | 62 | // ======================================== 63 | // COMPLEX BUSINESS LOGIC VALIDATION 64 | // ======================================== 65 | 66 | describe('Business Logic and Content Analysis', () => { 67 | test('should parse and validate SFCC debug log structure comprehensively', async () => { 68 | const result = await client.callTool('get_latest_debug', { limit: 5 }); 69 | 70 | validateDebugLogStructure(result, 5); 71 | 72 | const text = result.content[0].text; 73 | 74 | // Validate SFCC-specific patterns that require complex logic 75 | const sfccPatterns = [ 76 | /PipelineCallServlet|SystemJobThread/, 77 | /Sites-RefArchGlobal-Site/, 78 | /PipelineCall|custom \[\]/, 79 | /---/ // Entry separators 80 | ]; 81 | 82 | sfccPatterns.forEach((pattern, index) => { 83 | assert.ok(pattern.test(text), 84 | `Should contain SFCC pattern ${index}: ${pattern.toString()}`); 85 | }); 86 | 87 | // Validate chronological ordering (newest first) - complex logic 88 | const entries = text.split('---').filter(entry => entry.trim()); 89 | if (entries.length > 1) { 90 | const timestamps = entries.map(entry => { 91 | const match = entry.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT)\]/); 92 | return match ? new Date(match[1]) : null; 93 | }).filter(Boolean); 94 | 95 | for (let i = 1; i < timestamps.length; i++) { 96 | assert.ok(timestamps[i-1] >= timestamps[i], 97 | 'Debug entries should be in chronological order (newest first)'); 98 | } 99 | } 100 | }); 101 | 102 | test('should validate complex parameter combinations with business rules', async () => { 103 | const testScenarios = [ 104 | { 105 | params: { limit: 1 }, 106 | validator: (result) => { 107 | assert.ok(result.content[0].text.includes('Latest 1 debug messages')); 108 | assert.ok(!result.content[0].text.includes('---'), 109 | 'Single entry should not have separators'); 110 | } 111 | }, 112 | { 113 | params: { limit: 3, date: getCurrentDateString() }, 114 | validator: (result) => { 115 | assert.ok(result.content[0].text.includes('Latest 3 debug messages')); 116 | const separatorCount = (result.content[0].text.match(/---/g) || []).length; 117 | assert.ok(separatorCount >= 1, 'Multiple entries should have separators'); 118 | } 119 | }, 120 | { 121 | params: { limit: 20 }, 122 | validator: (result) => { 123 | assert.ok(result.content[0].text.includes('Latest 20 debug messages')); 124 | // Should handle larger limits gracefully 125 | assert.ok(result.content[0].text.length > 1000, 126 | 'Large limit should return substantial content'); 127 | } 128 | } 129 | ]; 130 | 131 | for (const scenario of testScenarios) { 132 | const result = await client.callTool('get_latest_debug', scenario.params); 133 | assertSuccessResponse(result); 134 | scenario.validator(result); 135 | } 136 | }); 137 | }); 138 | 139 | // ======================================== 140 | // DYNAMIC PARAMETER VALIDATION 141 | // ======================================== 142 | 143 | describe('Dynamic Parameter Validation', () => { 144 | test('should validate parameter types with complex edge case matrix', async () => { 145 | const testMatrix = [ 146 | // Valid scenarios 147 | { params: { limit: 5 }, expectSuccess: true, description: 'valid number limit' }, 148 | { params: { date: '20240101' }, expectSuccess: true, description: 'valid date format' }, 149 | { params: { limit: 1, date: getCurrentDateString() }, expectSuccess: true, description: 'valid combination' }, 150 | 151 | // Invalid scenarios requiring dynamic validation 152 | { params: { limit: '5' }, expectSuccess: false, expectedError: 'Invalid limit \'5\'', description: 'string limit' }, 153 | { params: { limit: 0 }, expectSuccess: false, expectedError: 'Invalid limit \'0\'', description: 'zero limit' }, 154 | { params: { limit: -1 }, expectSuccess: false, expectedError: 'Invalid limit', description: 'negative limit' }, 155 | { params: { limit: 9999 }, expectSuccess: false, expectedError: 'Invalid limit', description: 'excessive limit' }, 156 | 157 | // Complex type scenarios 158 | { params: { limit: null }, expectSuccess: true, description: 'null limit (uses default)' }, 159 | { params: { limit: [] }, expectSuccess: false, description: 'array limit' }, 160 | { params: { limit: {} }, expectSuccess: false, description: 'object limit' }, 161 | ]; 162 | 163 | for (const testCase of testMatrix) { 164 | const result = await client.callTool('get_latest_debug', testCase.params); 165 | assertValidMCPResponse(result); 166 | 167 | if (testCase.expectSuccess) { 168 | assert.equal(result.isError, false, 169 | `Should succeed for ${testCase.description}: ${JSON.stringify(testCase.params)}`); 170 | } else { 171 | assert.equal(result.isError, true, 172 | `Should fail for ${testCase.description}: ${JSON.stringify(testCase.params)}`); 173 | 174 | if (testCase.expectedError) { 175 | assert.ok(result.content[0].text.includes(testCase.expectedError), 176 | `Should contain expected error for ${testCase.description}`); 177 | } 178 | } 179 | } 180 | }); 181 | 182 | test('should handle date parameter edge cases with business logic', async () => { 183 | const dateScenarios = [ 184 | { date: 'invalid-date', expectMessage: 'No debug log files found' }, 185 | { date: '20261231', expectSuccess: true }, // Future date 186 | { date: '20240101', expectSuccess: true }, // Past date 187 | { date: 123, expectSuccess: true }, // Number converted to string 188 | ]; 189 | 190 | for (const scenario of dateScenarios) { 191 | const result = await client.callTool('get_latest_debug', { 192 | date: scenario.date, 193 | limit: 1 194 | }); 195 | 196 | assertValidMCPResponse(result); 197 | 198 | if (scenario.expectSuccess) { 199 | assert.equal(result.isError, false, 200 | `Date ${scenario.date} should be handled gracefully`); 201 | } 202 | 203 | if (scenario.expectMessage) { 204 | assert.ok(result.content[0].text.includes(scenario.expectMessage), 205 | `Should contain expected message for date ${scenario.date}`); 206 | } 207 | } 208 | }); 209 | }); 210 | 211 | // ======================================== 212 | // INTEGRATION AND WORKFLOW TESTING 213 | // ======================================== 214 | 215 | describe('Integration and Multi-Step Workflows', () => { 216 | test('should integrate with MCP protocol and tool ecosystem', async () => { 217 | // Step 1: Verify tool discovery 218 | const tools = await client.listTools(); 219 | const debugTool = tools.find(tool => tool.name === 'get_latest_debug'); 220 | 221 | assert.ok(debugTool, 'get_latest_debug tool should be available'); 222 | assert.ok(debugTool.description, 'Tool should have description'); 223 | assert.ok(debugTool.inputSchema, 'Tool should have input schema'); 224 | assert.equal(debugTool.inputSchema.type, 'object'); 225 | 226 | // Step 2: Validate schema properties 227 | const properties = debugTool.inputSchema.properties; 228 | assert.ok(properties.limit, 'Should have limit parameter in schema'); 229 | assert.ok(properties.date, 'Should have date parameter in schema'); 230 | 231 | // Step 3: Verify execution matches schema 232 | const result = await client.callTool('get_latest_debug', {}); 233 | assertValidMCPResponse(result); 234 | }); 235 | 236 | test('should maintain consistency with related log tools', async () => { 237 | // Get results from multiple log tools for comparison 238 | const debugResult = await client.callTool('get_latest_debug', { limit: 3 }); 239 | const infoResult = await client.callTool('get_latest_info', { limit: 3 }); 240 | 241 | assertValidMCPResponse(debugResult); 242 | assertValidMCPResponse(infoResult); 243 | 244 | // Validate structural consistency 245 | assert.equal(debugResult.content.length, infoResult.content.length, 246 | 'Debug and info tools should have similar response structure'); 247 | assert.equal(typeof debugResult.isError, typeof infoResult.isError, 248 | 'Both tools should handle errors consistently'); 249 | assert.equal(debugResult.content[0].type, infoResult.content[0].type, 250 | 'Content types should be consistent across log tools'); 251 | }); 252 | 253 | test('should support progressive parameter refinement workflow', async () => { 254 | // Workflow: Start broad, then narrow down 255 | 256 | // Step 1: Get initial debug overview 257 | const broadResult = await client.callTool('get_latest_debug', { limit: 10 }); 258 | assertSuccessResponse(broadResult); 259 | 260 | // Step 2: Narrow down to specific recent entries 261 | const recentResult = await client.callTool('get_latest_debug', { 262 | limit: 3, 263 | date: getCurrentDateString() 264 | }); 265 | assertSuccessResponse(recentResult); 266 | 267 | // Step 3: Focus on single most recent entry 268 | const focusedResult = await client.callTool('get_latest_debug', { limit: 1 }); 269 | assertSuccessResponse(focusedResult); 270 | 271 | // Validate progressive refinement logic 272 | assert.ok(broadResult.content[0].text.length >= recentResult.content[0].text.length, 273 | 'Broader search should return more content'); 274 | assert.ok(recentResult.content[0].text.length >= focusedResult.content[0].text.length, 275 | 'More focused search should return less content'); 276 | }); 277 | }); 278 | 279 | // ======================================== 280 | // ERROR RECOVERY AND RESILIENCE 281 | // ======================================== 282 | 283 | describe('Error Recovery and Resilience', () => { 284 | test('should recover gracefully from various error conditions', async () => { 285 | // Test sequence of various error conditions followed by recovery 286 | const errorSequence = [ 287 | { limit: 'invalid-string' }, 288 | { limit: -50 }, 289 | { limit: null }, 290 | { date: null, limit: 'bad' }, 291 | { unknownParam: 'test', limit: [] } 292 | ]; 293 | 294 | // Execute error sequence - should not break the server 295 | for (const errorParams of errorSequence) { 296 | const result = await client.callTool('get_latest_debug', errorParams); 297 | assertValidMCPResponse(result); 298 | // Don't assert success/failure as some may be handled gracefully 299 | } 300 | 301 | // Verify server recovers and works normally 302 | const recoveryResult = await client.callTool('get_latest_debug', { limit: 2 }); 303 | assertSuccessResponse(recoveryResult); 304 | validateDebugLogStructure(recoveryResult, 2); 305 | }); 306 | 307 | test('should maintain state consistency across sequential operations', async () => { 308 | // Test that multiple operations don't interfere with each other 309 | const operations = [ 310 | { limit: 1 }, 311 | { limit: 5 }, 312 | { date: getCurrentDateString(), limit: 2 }, 313 | { limit: 10 }, 314 | { date: '20240101', limit: 1 } 315 | ]; 316 | 317 | const results = []; 318 | 319 | // Execute operations sequentially (no concurrent requests per AGENTS.md) 320 | for (const params of operations) { 321 | const result = await client.callTool('get_latest_debug', params); 322 | results.push({ params, result }); 323 | } 324 | 325 | // Validate each operation succeeded independently 326 | results.forEach(({ params, result }, index) => { 327 | assertValidMCPResponse(result); 328 | assert.equal(result.isError, false, 329 | `Operation ${index + 1} should succeed: ${JSON.stringify(params)}`); 330 | 331 | // Validate response format consistency 332 | assert.equal(result.content.length, 1, 'Should have exactly one content item'); 333 | assert.equal(result.content[0].type, 'text', 'Content type should be text'); 334 | assert.ok(result.content[0].text.length > 0, 'Content should not be empty'); 335 | }); 336 | }); 337 | }); 338 | }); 339 | ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/list-sfcc-classes.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | # ================================================================================== 2 | # SFCC MCP Server - list_sfcc_classes Tool YAML Tests 3 | # Comprehensive testing for SFCC class listing functionality 4 | # Tests both successful responses and performance validation 5 | # 6 | # Quick Test Commands: 7 | # aegis "tests/mcp/yaml/list-sfcc-classes.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --verbose 8 | # aegis "tests/mcp/yaml/list-sfcc-classes.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --debug --timing 9 | # aegis query list_sfcc_classes '{}' --config "aegis.config.with-dw.json" 10 | # ================================================================================== 11 | description: "SFCC MCP Server list_sfcc_classes tool - comprehensive validation" 12 | 13 | # ================================================================================== 14 | # BASIC TOOL STRUCTURE VALIDATION 15 | # ================================================================================== 16 | tests: 17 | - it: "should list list_sfcc_classes tool in available tools" 18 | request: 19 | jsonrpc: "2.0" 20 | id: "tool-available" 21 | method: "tools/list" 22 | params: {} 23 | expect: 24 | response: 25 | jsonrpc: "2.0" 26 | id: "tool-available" 27 | result: 28 | match:extractField: "tools.*.name" 29 | value: "match:arrayContains:list_sfcc_classes" 30 | stderr: "toBeEmpty" 31 | 32 | - it: "should have list_sfcc_classes in tools list with proper structure" 33 | request: 34 | jsonrpc: "2.0" 35 | id: "tool-metadata" 36 | method: "tools/list" 37 | params: {} 38 | expect: 39 | response: 40 | jsonrpc: "2.0" 41 | id: "tool-metadata" 42 | result: 43 | tools: "match:arrayContains:name:list_sfcc_classes" 44 | stderr: "toBeEmpty" 45 | 46 | # ================================================================================== 47 | # SUCCESSFUL EXECUTION TESTS - NO PARAMETERS NEEDED 48 | # ================================================================================== 49 | 50 | - it: "should execute with no parameters (empty object)" 51 | request: 52 | jsonrpc: "2.0" 53 | id: "exec-no-params" 54 | method: "tools/call" 55 | params: 56 | name: "list_sfcc_classes" 57 | arguments: {} 58 | expect: 59 | response: 60 | jsonrpc: "2.0" 61 | id: "exec-no-params" 62 | result: 63 | content: 64 | - type: "text" 65 | text: "match:type:string" 66 | isError: false 67 | stderr: "toBeEmpty" 68 | 69 | - it: "should return comprehensive class list structure" 70 | request: 71 | jsonrpc: "2.0" 72 | id: "exec-structure-validation" 73 | method: "tools/call" 74 | params: 75 | name: "list_sfcc_classes" 76 | arguments: {} 77 | expect: 78 | response: 79 | jsonrpc: "2.0" 80 | id: "exec-structure-validation" 81 | result: 82 | content: 83 | - type: "text" 84 | text: "match:contains:TopLevel" 85 | isError: false 86 | stderr: "toBeEmpty" 87 | 88 | - it: "should include core SFCC classes in response" 89 | request: 90 | jsonrpc: "2.0" 91 | id: "exec-core-classes" 92 | method: "tools/call" 93 | params: 94 | name: "list_sfcc_classes" 95 | arguments: {} 96 | expect: 97 | response: 98 | jsonrpc: "2.0" 99 | id: "exec-core-classes" 100 | result: 101 | content: 102 | - type: "text" 103 | text: "match:contains:dw.catalog" 104 | isError: false 105 | stderr: "toBeEmpty" 106 | 107 | - it: "should include system classes in response" 108 | request: 109 | jsonrpc: "2.0" 110 | id: "exec-system-classes" 111 | method: "tools/call" 112 | params: 113 | name: "list_sfcc_classes" 114 | arguments: {} 115 | expect: 116 | response: 117 | jsonrpc: "2.0" 118 | id: "exec-system-classes" 119 | result: 120 | content: 121 | - type: "text" 122 | text: "match:contains:dw.system" 123 | isError: false 124 | stderr: "toBeEmpty" 125 | 126 | - it: "should include order classes in response" 127 | request: 128 | jsonrpc: "2.0" 129 | id: "exec-order-classes" 130 | method: "tools/call" 131 | params: 132 | name: "list_sfcc_classes" 133 | arguments: {} 134 | expect: 135 | response: 136 | jsonrpc: "2.0" 137 | id: "exec-order-classes" 138 | result: 139 | content: 140 | - type: "text" 141 | text: "match:contains:dw.order" 142 | isError: false 143 | stderr: "toBeEmpty" 144 | 145 | - it: "should include customer classes in response" 146 | request: 147 | jsonrpc: "2.0" 148 | id: "exec-customer-classes" 149 | method: "tools/call" 150 | params: 151 | name: "list_sfcc_classes" 152 | arguments: {} 153 | expect: 154 | response: 155 | jsonrpc: "2.0" 156 | id: "exec-customer-classes" 157 | result: 158 | content: 159 | - type: "text" 160 | text: "match:contains:dw.customer" 161 | isError: false 162 | stderr: "toBeEmpty" 163 | 164 | # ================================================================================== 165 | # RESPONSE CONTENT VALIDATION 166 | # ================================================================================== 167 | 168 | - it: "should return substantial class list (not empty)" 169 | request: 170 | jsonrpc: "2.0" 171 | id: "content-substantial" 172 | method: "tools/call" 173 | params: 174 | name: "list_sfcc_classes" 175 | arguments: {} 176 | expect: 177 | response: 178 | jsonrpc: "2.0" 179 | id: "content-substantial" 180 | result: 181 | content: 182 | - type: "text" 183 | text: "match:contains:dw.catalog.Product" # Should contain specific class 184 | isError: false 185 | stderr: "toBeEmpty" 186 | 187 | - it: "should contain structured class information" 188 | request: 189 | jsonrpc: "2.0" 190 | id: "content-structured" 191 | method: "tools/call" 192 | params: 193 | name: "list_sfcc_classes" 194 | arguments: {} 195 | expect: 196 | response: 197 | jsonrpc: "2.0" 198 | id: "content-structured" 199 | result: 200 | content: 201 | - type: "text" 202 | text: "match:regex:dw\\.[a-zA-Z]+" # Should contain dw.* patterns 203 | isError: false 204 | stderr: "toBeEmpty" 205 | 206 | - it: "should include SFCC namespace classes" 207 | request: 208 | jsonrpc: "2.0" 209 | id: "content-hierarchy" 210 | method: "tools/call" 211 | params: 212 | name: "list_sfcc_classes" 213 | arguments: {} 214 | expect: 215 | response: 216 | jsonrpc: "2.0" 217 | id: "content-hierarchy" 218 | result: 219 | content: 220 | - type: "text" 221 | text: "match:contains:dw.catalog" 222 | isError: false 223 | stderr: "toBeEmpty" 224 | 225 | # ================================================================================== 226 | # PERFORMANCE VALIDATION 227 | # ================================================================================== 228 | 229 | - it: "should complete class listing within reasonable time (metadata operation)" 230 | request: 231 | jsonrpc: "2.0" 232 | id: "perf-metadata-fast" 233 | method: "tools/call" 234 | params: 235 | name: "list_sfcc_classes" 236 | arguments: {} 237 | expect: 238 | response: 239 | jsonrpc: "2.0" 240 | id: "perf-metadata-fast" 241 | result: 242 | content: 243 | - type: "text" 244 | text: "match:type:string" 245 | isError: false 246 | performance: 247 | maxResponseTime: "1000ms" # Should be fast for metadata 248 | stderr: "toBeEmpty" 249 | 250 | - it: "should handle class listing efficiently" 251 | request: 252 | jsonrpc: "2.0" 253 | id: "perf-efficient" 254 | method: "tools/call" 255 | params: 256 | name: "list_sfcc_classes" 257 | arguments: {} 258 | expect: 259 | response: 260 | jsonrpc: "2.0" 261 | id: "perf-efficient" 262 | result: 263 | content: "match:type:array" 264 | isError: false 265 | performance: 266 | maxResponseTime: "2000ms" # Generous for comprehensive listing 267 | stderr: "toBeEmpty" 268 | 269 | # ================================================================================== 270 | # CONTENT QUALITY VALIDATION 271 | # ================================================================================== 272 | 273 | - it: "should include comprehensive namespace coverage" 274 | request: 275 | jsonrpc: "2.0" 276 | id: "namespace-coverage" 277 | method: "tools/call" 278 | params: 279 | name: "list_sfcc_classes" 280 | arguments: {} 281 | expect: 282 | response: 283 | jsonrpc: "2.0" 284 | id: "namespace-coverage" 285 | result: 286 | content: 287 | - type: "text" 288 | text: "match:contains:dw.web" 289 | isError: false 290 | stderr: "toBeEmpty" 291 | 292 | - it: "should include utility classes" 293 | request: 294 | jsonrpc: "2.0" 295 | id: "utility-classes" 296 | method: "tools/call" 297 | params: 298 | name: "list_sfcc_classes" 299 | arguments: {} 300 | expect: 301 | response: 302 | jsonrpc: "2.0" 303 | id: "utility-classes" 304 | result: 305 | content: 306 | - type: "text" 307 | text: "match:contains:dw.util" 308 | isError: false 309 | stderr: "toBeEmpty" 310 | 311 | - it: "should include IO classes" 312 | request: 313 | jsonrpc: "2.0" 314 | id: "io-classes" 315 | method: "tools/call" 316 | params: 317 | name: "list_sfcc_classes" 318 | arguments: {} 319 | expect: 320 | response: 321 | jsonrpc: "2.0" 322 | id: "io-classes" 323 | result: 324 | content: 325 | - type: "text" 326 | text: "match:contains:dw.io" 327 | isError: false 328 | stderr: "toBeEmpty" 329 | 330 | - it: "should include service classes" 331 | request: 332 | jsonrpc: "2.0" 333 | id: "service-classes" 334 | method: "tools/call" 335 | params: 336 | name: "list_sfcc_classes" 337 | arguments: {} 338 | expect: 339 | response: 340 | jsonrpc: "2.0" 341 | id: "service-classes" 342 | result: 343 | content: 344 | - type: "text" 345 | text: "match:contains:dw.svc" 346 | isError: false 347 | stderr: "toBeEmpty" 348 | 349 | # ================================================================================== 350 | # ERROR HANDLING & EDGE CASES 351 | # ================================================================================== 352 | 353 | - it: "should handle call with empty arguments gracefully" 354 | request: 355 | jsonrpc: "2.0" 356 | id: "empty-args" 357 | method: "tools/call" 358 | params: 359 | name: "list_sfcc_classes" 360 | arguments: {} 361 | expect: 362 | response: 363 | jsonrpc: "2.0" 364 | id: "empty-args" 365 | result: 366 | content: 367 | - type: "text" 368 | text: "match:type:string" 369 | isError: false 370 | stderr: "toBeEmpty" 371 | 372 | - it: "should handle empty arguments object consistently" 373 | request: 374 | jsonrpc: "2.0" 375 | id: "empty-args-consistent" 376 | method: "tools/call" 377 | params: 378 | name: "list_sfcc_classes" 379 | arguments: {} 380 | expect: 381 | response: 382 | jsonrpc: "2.0" 383 | id: "empty-args-consistent" 384 | result: 385 | content: 386 | - type: "text" 387 | text: "match:not:contains:error" 388 | isError: false 389 | stderr: "toBeEmpty" 390 | 391 | - it: "should ignore additional parameters gracefully" 392 | request: 393 | jsonrpc: "2.0" 394 | id: "extra-params" 395 | method: "tools/call" 396 | params: 397 | name: "list_sfcc_classes" 398 | arguments: 399 | unexpectedParam: "should be ignored" 400 | anotherParam: 123 401 | expect: 402 | response: 403 | jsonrpc: "2.0" 404 | id: "extra-params" 405 | result: 406 | content: 407 | - type: "text" 408 | text: "match:type:string" 409 | isError: false 410 | stderr: "toBeEmpty" 411 | 412 | # ================================================================================== 413 | # COMPREHENSIVE CONTENT VALIDATION 414 | # ================================================================================== 415 | 416 | - it: "should provide educational content for new developers" 417 | request: 418 | jsonrpc: "2.0" 419 | id: "educational-content" 420 | method: "tools/call" 421 | params: 422 | name: "list_sfcc_classes" 423 | arguments: {} 424 | expect: 425 | response: 426 | jsonrpc: "2.0" 427 | id: "educational-content" 428 | result: 429 | content: 430 | - type: "text" 431 | text: "match:contains:dw." 432 | isError: false 433 | stderr: "toBeEmpty" 434 | 435 | - it: "should include discovery-friendly information" 436 | request: 437 | jsonrpc: "2.0" 438 | id: "discovery-friendly" 439 | method: "tools/call" 440 | params: 441 | name: "list_sfcc_classes" 442 | arguments: {} 443 | expect: 444 | response: 445 | jsonrpc: "2.0" 446 | id: "discovery-friendly" 447 | result: 448 | content: 449 | - type: "text" 450 | text: "match:not:contains:undefined" 451 | isError: false 452 | stderr: "toBeEmpty" 453 | 454 | - it: "should be useful for SFCC API exploration" 455 | request: 456 | jsonrpc: "2.0" 457 | id: "api-exploration" 458 | method: "tools/call" 459 | params: 460 | name: "list_sfcc_classes" 461 | arguments: {} 462 | expect: 463 | response: 464 | jsonrpc: "2.0" 465 | id: "api-exploration" 466 | result: 467 | content: 468 | - type: "text" 469 | text: "match:contains:dw.system.Site" # Should contain specific class 470 | isError: false 471 | stderr: "toBeEmpty" 472 | 473 | # ================================================================================== 474 | # FINAL INTEGRATION VALIDATION 475 | # ================================================================================== 476 | 477 | - it: "should provide consistent response structure across calls" 478 | request: 479 | jsonrpc: "2.0" 480 | id: "consistency-check" 481 | method: "tools/call" 482 | params: 483 | name: "list_sfcc_classes" 484 | arguments: {} 485 | expect: 486 | response: 487 | jsonrpc: "2.0" 488 | id: "consistency-check" 489 | result: 490 | content: "match:type:array" 491 | isError: false 492 | stderr: "toBeEmpty" 493 | 494 | - it: "should serve as good starting point for SFCC development" 495 | request: 496 | jsonrpc: "2.0" 497 | id: "development-starting-point" 498 | method: "tools/call" 499 | params: 500 | name: "list_sfcc_classes" 501 | arguments: {} 502 | expect: 503 | response: 504 | jsonrpc: "2.0" 505 | id: "development-starting-point" 506 | result: 507 | content: 508 | - type: "text" 509 | text: "match:contains:TopLevel" 510 | isError: false 511 | stderr: "toBeEmpty" 512 | ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/list-sfcc-classes.docs-only.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | # ================================================================================== 2 | # SFCC MCP Server - list_sfcc_classes Tool YAML Tests 3 | # Comprehensive testing for SFCC class listing functionality 4 | # Tests both successful responses and performance validation 5 | # 6 | # Quick Test Commands: 7 | # aegis "tests/mcp/yaml/list-sfcc-classes.docs-only.test.mcp.yml" --config "aegis.config.docs-only.json" --verbose 8 | # aegis "tests/mcp/yaml/list-sfcc-classes.docs-only.test.mcp.yml" --config "aegis.config.docs-only.json" --debug --timing 9 | # aegis query list_sfcc_classes '{}' --config "aegis.config.docs-only.json" 10 | # ================================================================================== 11 | description: "SFCC MCP Server list_sfcc_classes tool - comprehensive validation" 12 | 13 | # ================================================================================== 14 | # BASIC TOOL STRUCTURE VALIDATION 15 | # ================================================================================== 16 | tests: 17 | - it: "should list list_sfcc_classes tool in available tools" 18 | request: 19 | jsonrpc: "2.0" 20 | id: "tool-available" 21 | method: "tools/list" 22 | params: {} 23 | expect: 24 | response: 25 | jsonrpc: "2.0" 26 | id: "tool-available" 27 | result: 28 | match:extractField: "tools.*.name" 29 | value: "match:arrayContains:list_sfcc_classes" 30 | stderr: "toBeEmpty" 31 | 32 | - it: "should have list_sfcc_classes in tools list with proper structure" 33 | request: 34 | jsonrpc: "2.0" 35 | id: "tool-metadata" 36 | method: "tools/list" 37 | params: {} 38 | expect: 39 | response: 40 | jsonrpc: "2.0" 41 | id: "tool-metadata" 42 | result: 43 | tools: "match:arrayContains:name:list_sfcc_classes" 44 | stderr: "toBeEmpty" 45 | 46 | # ================================================================================== 47 | # SUCCESSFUL EXECUTION TESTS - NO PARAMETERS NEEDED 48 | # ================================================================================== 49 | 50 | - it: "should execute with no parameters (empty object)" 51 | request: 52 | jsonrpc: "2.0" 53 | id: "exec-no-params" 54 | method: "tools/call" 55 | params: 56 | name: "list_sfcc_classes" 57 | arguments: {} 58 | expect: 59 | response: 60 | jsonrpc: "2.0" 61 | id: "exec-no-params" 62 | result: 63 | content: 64 | - type: "text" 65 | text: "match:type:string" 66 | isError: false 67 | stderr: "toBeEmpty" 68 | 69 | - it: "should return comprehensive class list structure" 70 | request: 71 | jsonrpc: "2.0" 72 | id: "exec-structure-validation" 73 | method: "tools/call" 74 | params: 75 | name: "list_sfcc_classes" 76 | arguments: {} 77 | expect: 78 | response: 79 | jsonrpc: "2.0" 80 | id: "exec-structure-validation" 81 | result: 82 | content: 83 | - type: "text" 84 | text: "match:contains:TopLevel" 85 | isError: false 86 | stderr: "toBeEmpty" 87 | 88 | - it: "should include core SFCC classes in response" 89 | request: 90 | jsonrpc: "2.0" 91 | id: "exec-core-classes" 92 | method: "tools/call" 93 | params: 94 | name: "list_sfcc_classes" 95 | arguments: {} 96 | expect: 97 | response: 98 | jsonrpc: "2.0" 99 | id: "exec-core-classes" 100 | result: 101 | content: 102 | - type: "text" 103 | text: "match:contains:dw.catalog" 104 | isError: false 105 | stderr: "toBeEmpty" 106 | 107 | - it: "should include system classes in response" 108 | request: 109 | jsonrpc: "2.0" 110 | id: "exec-system-classes" 111 | method: "tools/call" 112 | params: 113 | name: "list_sfcc_classes" 114 | arguments: {} 115 | expect: 116 | response: 117 | jsonrpc: "2.0" 118 | id: "exec-system-classes" 119 | result: 120 | content: 121 | - type: "text" 122 | text: "match:contains:dw.system" 123 | isError: false 124 | stderr: "toBeEmpty" 125 | 126 | - it: "should include order classes in response" 127 | request: 128 | jsonrpc: "2.0" 129 | id: "exec-order-classes" 130 | method: "tools/call" 131 | params: 132 | name: "list_sfcc_classes" 133 | arguments: {} 134 | expect: 135 | response: 136 | jsonrpc: "2.0" 137 | id: "exec-order-classes" 138 | result: 139 | content: 140 | - type: "text" 141 | text: "match:contains:dw.order" 142 | isError: false 143 | stderr: "toBeEmpty" 144 | 145 | - it: "should include customer classes in response" 146 | request: 147 | jsonrpc: "2.0" 148 | id: "exec-customer-classes" 149 | method: "tools/call" 150 | params: 151 | name: "list_sfcc_classes" 152 | arguments: {} 153 | expect: 154 | response: 155 | jsonrpc: "2.0" 156 | id: "exec-customer-classes" 157 | result: 158 | content: 159 | - type: "text" 160 | text: "match:contains:dw.customer" 161 | isError: false 162 | stderr: "toBeEmpty" 163 | 164 | # ================================================================================== 165 | # RESPONSE CONTENT VALIDATION 166 | # ================================================================================== 167 | 168 | - it: "should return substantial class list (not empty)" 169 | request: 170 | jsonrpc: "2.0" 171 | id: "content-substantial" 172 | method: "tools/call" 173 | params: 174 | name: "list_sfcc_classes" 175 | arguments: {} 176 | expect: 177 | response: 178 | jsonrpc: "2.0" 179 | id: "content-substantial" 180 | result: 181 | content: 182 | - type: "text" 183 | text: "match:contains:dw.catalog.Product" # Should contain specific class 184 | isError: false 185 | stderr: "toBeEmpty" 186 | 187 | - it: "should contain structured class information" 188 | request: 189 | jsonrpc: "2.0" 190 | id: "content-structured" 191 | method: "tools/call" 192 | params: 193 | name: "list_sfcc_classes" 194 | arguments: {} 195 | expect: 196 | response: 197 | jsonrpc: "2.0" 198 | id: "content-structured" 199 | result: 200 | content: 201 | - type: "text" 202 | text: "match:regex:dw\\.[a-zA-Z]+" # Should contain dw.* patterns 203 | isError: false 204 | stderr: "toBeEmpty" 205 | 206 | - it: "should include SFCC namespace classes" 207 | request: 208 | jsonrpc: "2.0" 209 | id: "content-hierarchy" 210 | method: "tools/call" 211 | params: 212 | name: "list_sfcc_classes" 213 | arguments: {} 214 | expect: 215 | response: 216 | jsonrpc: "2.0" 217 | id: "content-hierarchy" 218 | result: 219 | content: 220 | - type: "text" 221 | text: "match:contains:dw.catalog" 222 | isError: false 223 | stderr: "toBeEmpty" 224 | 225 | # ================================================================================== 226 | # PERFORMANCE VALIDATION 227 | # ================================================================================== 228 | 229 | - it: "should complete class listing within reasonable time (metadata operation)" 230 | request: 231 | jsonrpc: "2.0" 232 | id: "perf-metadata-fast" 233 | method: "tools/call" 234 | params: 235 | name: "list_sfcc_classes" 236 | arguments: {} 237 | expect: 238 | response: 239 | jsonrpc: "2.0" 240 | id: "perf-metadata-fast" 241 | result: 242 | content: 243 | - type: "text" 244 | text: "match:type:string" 245 | isError: false 246 | performance: 247 | maxResponseTime: "1000ms" # Should be fast for metadata 248 | stderr: "toBeEmpty" 249 | 250 | - it: "should handle class listing efficiently" 251 | request: 252 | jsonrpc: "2.0" 253 | id: "perf-efficient" 254 | method: "tools/call" 255 | params: 256 | name: "list_sfcc_classes" 257 | arguments: {} 258 | expect: 259 | response: 260 | jsonrpc: "2.0" 261 | id: "perf-efficient" 262 | result: 263 | content: "match:type:array" 264 | isError: false 265 | performance: 266 | maxResponseTime: "2000ms" # Generous for comprehensive listing 267 | stderr: "toBeEmpty" 268 | 269 | # ================================================================================== 270 | # CONTENT QUALITY VALIDATION 271 | # ================================================================================== 272 | 273 | - it: "should include comprehensive namespace coverage" 274 | request: 275 | jsonrpc: "2.0" 276 | id: "namespace-coverage" 277 | method: "tools/call" 278 | params: 279 | name: "list_sfcc_classes" 280 | arguments: {} 281 | expect: 282 | response: 283 | jsonrpc: "2.0" 284 | id: "namespace-coverage" 285 | result: 286 | content: 287 | - type: "text" 288 | text: "match:contains:dw.web" 289 | isError: false 290 | stderr: "toBeEmpty" 291 | 292 | - it: "should include utility classes" 293 | request: 294 | jsonrpc: "2.0" 295 | id: "utility-classes" 296 | method: "tools/call" 297 | params: 298 | name: "list_sfcc_classes" 299 | arguments: {} 300 | expect: 301 | response: 302 | jsonrpc: "2.0" 303 | id: "utility-classes" 304 | result: 305 | content: 306 | - type: "text" 307 | text: "match:contains:dw.util" 308 | isError: false 309 | stderr: "toBeEmpty" 310 | 311 | - it: "should include IO classes" 312 | request: 313 | jsonrpc: "2.0" 314 | id: "io-classes" 315 | method: "tools/call" 316 | params: 317 | name: "list_sfcc_classes" 318 | arguments: {} 319 | expect: 320 | response: 321 | jsonrpc: "2.0" 322 | id: "io-classes" 323 | result: 324 | content: 325 | - type: "text" 326 | text: "match:contains:dw.io" 327 | isError: false 328 | stderr: "toBeEmpty" 329 | 330 | - it: "should include service classes" 331 | request: 332 | jsonrpc: "2.0" 333 | id: "service-classes" 334 | method: "tools/call" 335 | params: 336 | name: "list_sfcc_classes" 337 | arguments: {} 338 | expect: 339 | response: 340 | jsonrpc: "2.0" 341 | id: "service-classes" 342 | result: 343 | content: 344 | - type: "text" 345 | text: "match:contains:dw.svc" 346 | isError: false 347 | stderr: "toBeEmpty" 348 | 349 | # ================================================================================== 350 | # ERROR HANDLING & EDGE CASES 351 | # ================================================================================== 352 | 353 | - it: "should handle call with empty arguments gracefully" 354 | request: 355 | jsonrpc: "2.0" 356 | id: "empty-args" 357 | method: "tools/call" 358 | params: 359 | name: "list_sfcc_classes" 360 | arguments: {} 361 | expect: 362 | response: 363 | jsonrpc: "2.0" 364 | id: "empty-args" 365 | result: 366 | content: 367 | - type: "text" 368 | text: "match:type:string" 369 | isError: false 370 | stderr: "toBeEmpty" 371 | 372 | - it: "should handle empty arguments object consistently" 373 | request: 374 | jsonrpc: "2.0" 375 | id: "empty-args-consistent" 376 | method: "tools/call" 377 | params: 378 | name: "list_sfcc_classes" 379 | arguments: {} 380 | expect: 381 | response: 382 | jsonrpc: "2.0" 383 | id: "empty-args-consistent" 384 | result: 385 | content: 386 | - type: "text" 387 | text: "match:not:contains:error" 388 | isError: false 389 | stderr: "toBeEmpty" 390 | 391 | - it: "should ignore additional parameters gracefully" 392 | request: 393 | jsonrpc: "2.0" 394 | id: "extra-params" 395 | method: "tools/call" 396 | params: 397 | name: "list_sfcc_classes" 398 | arguments: 399 | unexpectedParam: "should be ignored" 400 | anotherParam: 123 401 | expect: 402 | response: 403 | jsonrpc: "2.0" 404 | id: "extra-params" 405 | result: 406 | content: 407 | - type: "text" 408 | text: "match:type:string" 409 | isError: false 410 | stderr: "toBeEmpty" 411 | 412 | # ================================================================================== 413 | # COMPREHENSIVE CONTENT VALIDATION 414 | # ================================================================================== 415 | 416 | - it: "should provide educational content for new developers" 417 | request: 418 | jsonrpc: "2.0" 419 | id: "educational-content" 420 | method: "tools/call" 421 | params: 422 | name: "list_sfcc_classes" 423 | arguments: {} 424 | expect: 425 | response: 426 | jsonrpc: "2.0" 427 | id: "educational-content" 428 | result: 429 | content: 430 | - type: "text" 431 | text: "match:contains:dw." 432 | isError: false 433 | stderr: "toBeEmpty" 434 | 435 | - it: "should include discovery-friendly information" 436 | request: 437 | jsonrpc: "2.0" 438 | id: "discovery-friendly" 439 | method: "tools/call" 440 | params: 441 | name: "list_sfcc_classes" 442 | arguments: {} 443 | expect: 444 | response: 445 | jsonrpc: "2.0" 446 | id: "discovery-friendly" 447 | result: 448 | content: 449 | - type: "text" 450 | text: "match:not:contains:undefined" 451 | isError: false 452 | stderr: "toBeEmpty" 453 | 454 | - it: "should be useful for SFCC API exploration" 455 | request: 456 | jsonrpc: "2.0" 457 | id: "api-exploration" 458 | method: "tools/call" 459 | params: 460 | name: "list_sfcc_classes" 461 | arguments: {} 462 | expect: 463 | response: 464 | jsonrpc: "2.0" 465 | id: "api-exploration" 466 | result: 467 | content: 468 | - type: "text" 469 | text: "match:contains:dw.system.Site" # Should contain specific class 470 | isError: false 471 | stderr: "toBeEmpty" 472 | 473 | # ================================================================================== 474 | # FINAL INTEGRATION VALIDATION 475 | # ================================================================================== 476 | 477 | - it: "should provide consistent response structure across calls" 478 | request: 479 | jsonrpc: "2.0" 480 | id: "consistency-check" 481 | method: "tools/call" 482 | params: 483 | name: "list_sfcc_classes" 484 | arguments: {} 485 | expect: 486 | response: 487 | jsonrpc: "2.0" 488 | id: "consistency-check" 489 | result: 490 | content: "match:type:array" 491 | isError: false 492 | stderr: "toBeEmpty" 493 | 494 | - it: "should serve as good starting point for SFCC development" 495 | request: 496 | jsonrpc: "2.0" 497 | id: "development-starting-point" 498 | method: "tools/call" 499 | params: 500 | name: "list_sfcc_classes" 501 | arguments: {} 502 | expect: 503 | response: 504 | jsonrpc: "2.0" 505 | id: "development-starting-point" 506 | result: 507 | content: 508 | - type: "text" 509 | text: "match:contains:TopLevel" 510 | isError: false 511 | stderr: "toBeEmpty" 512 | ``` -------------------------------------------------------------------------------- /docs/dw_io/XMLStreamWriter.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.io 2 | 3 | # Class XMLStreamWriter 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.io.XMLStreamWriter 9 | 10 | ## Description 11 | 12 | The XMLStreamWriter can be used to write small and large XML feeds. Note: when this class is used with sensitive data, be careful in persisting sensitive information to disk. The XMLStreamWriter does not perform well-formedness checking on its input. However the writeCharacters method escapes '&' , '<' and '>'. For attribute values the writeAttribute method escapes the above characters plus '"' to ensure that all character content and attribute values are well formed. The following example illustrates how to use this class: var fileWriter : FileWriter = new FileWriter(file, "UTF-8"); var xsw : XMLStreamWriter = new XMLStreamWriter(fileWriter); xsw.writeStartDocument(); xsw.writeStartElement("products"); xsw.writeStartElement("product"); xsw.writeAttribute("id", "p42"); xsw.writeStartElement("name"); xsw.writeCharacters("blue t-shirt"); xsw.writeEndElement(); xsw.writeStartElement("rating"); xsw.writeCharacters("2.0"); xsw.writeEndElement(); xsw.writeEndElement(); xsw.writeEndElement(); xsw.writeEndDocument(); xsw.close(); fileWriter.close(); The code above will write the following to file: <?xml version="1.0" ?> <products> <product id="p42"> <name>a blue t-shirt</name> <rating>2.0</rating> </product> </products> Note: This output has been formatted for readability. See XMLIndentingStreamWriter. 13 | 14 | ## Properties 15 | 16 | ### defaultNamespace 17 | 18 | **Type:** String 19 | 20 | The current default name space. 21 | 22 | ## Constructor Summary 23 | 24 | XMLStreamWriter(writer : Writer) Constructs the XMLStreamWriter for a writer. 25 | 26 | ## Method Summary 27 | 28 | ### close 29 | 30 | **Signature:** `close() : void` 31 | 32 | Close this writer and free any resources associated with the writer. 33 | 34 | ### flush 35 | 36 | **Signature:** `flush() : void` 37 | 38 | Write any cached data to the underlying output mechanism. 39 | 40 | ### getDefaultNamespace 41 | 42 | **Signature:** `getDefaultNamespace() : String` 43 | 44 | Returns the current default name space. 45 | 46 | ### getPrefix 47 | 48 | **Signature:** `getPrefix(uri : String) : String` 49 | 50 | Gets the prefix the URI is bound to. 51 | 52 | ### setDefaultNamespace 53 | 54 | **Signature:** `setDefaultNamespace(uri : String) : void` 55 | 56 | Binds a URI to the default namespace. 57 | 58 | ### setPrefix 59 | 60 | **Signature:** `setPrefix(prefix : String, uri : String) : void` 61 | 62 | Sets the prefix the uri is bound to. 63 | 64 | ### writeAttribute 65 | 66 | **Signature:** `writeAttribute(localName : String, value : String) : void` 67 | 68 | Writes an attribute to the output stream without a prefix. 69 | 70 | ### writeAttribute 71 | 72 | **Signature:** `writeAttribute(prefix : String, namespaceURI : String, localName : String, value : String) : void` 73 | 74 | Writes an attribute to the output stream. 75 | 76 | ### writeAttribute 77 | 78 | **Signature:** `writeAttribute(namespaceURI : String, localName : String, value : String) : void` 79 | 80 | Writes an attribute to the output stream. 81 | 82 | ### writeCData 83 | 84 | **Signature:** `writeCData(data : String) : void` 85 | 86 | Writes a CData section. 87 | 88 | ### writeCharacters 89 | 90 | **Signature:** `writeCharacters(text : String) : void` 91 | 92 | Write text to the output. 93 | 94 | ### writeComment 95 | 96 | **Signature:** `writeComment(data : String) : void` 97 | 98 | Writes an XML comment with the data enclosed. 99 | 100 | ### writeDefaultNamespace 101 | 102 | **Signature:** `writeDefaultNamespace(namespaceURI : String) : void` 103 | 104 | Writes the default namespace to the stream. 105 | 106 | ### writeDTD 107 | 108 | **Signature:** `writeDTD(dtd : String) : void` 109 | 110 | Write a DTD section. 111 | 112 | ### writeEmptyElement 113 | 114 | **Signature:** `writeEmptyElement(namespaceURI : String, localName : String) : void` 115 | 116 | Writes an empty element tag to the output. 117 | 118 | ### writeEmptyElement 119 | 120 | **Signature:** `writeEmptyElement(prefix : String, localName : String, namespaceURI : String) : void` 121 | 122 | Writes an empty element tag to the output. 123 | 124 | ### writeEmptyElement 125 | 126 | **Signature:** `writeEmptyElement(localName : String) : void` 127 | 128 | Writes an empty element tag to the output. 129 | 130 | ### writeEndDocument 131 | 132 | **Signature:** `writeEndDocument() : void` 133 | 134 | Closes any start tags and writes corresponding end tags. 135 | 136 | ### writeEndElement 137 | 138 | **Signature:** `writeEndElement() : void` 139 | 140 | Writes an end tag to the output relying on the internal state of the writer to determine the prefix and local name of the event. 141 | 142 | ### writeEntityRef 143 | 144 | **Signature:** `writeEntityRef(name : String) : void` 145 | 146 | Writes an entity reference. 147 | 148 | ### writeNamespace 149 | 150 | **Signature:** `writeNamespace(prefix : String, namespaceURI : String) : void` 151 | 152 | Writes a namespace to the output stream. 153 | 154 | ### writeProcessingInstruction 155 | 156 | **Signature:** `writeProcessingInstruction(target : String) : void` 157 | 158 | Writes a processing instruction. 159 | 160 | ### writeProcessingInstruction 161 | 162 | **Signature:** `writeProcessingInstruction(target : String, data : String) : void` 163 | 164 | Writes a processing instruction. 165 | 166 | ### writeRaw 167 | 168 | **Signature:** `writeRaw(raw : String) : void` 169 | 170 | Writes the given string directly into the output stream. 171 | 172 | ### writeStartDocument 173 | 174 | **Signature:** `writeStartDocument() : void` 175 | 176 | Write the XML Declaration. 177 | 178 | ### writeStartDocument 179 | 180 | **Signature:** `writeStartDocument(version : String) : void` 181 | 182 | Write the XML Declaration. 183 | 184 | ### writeStartDocument 185 | 186 | **Signature:** `writeStartDocument(encoding : String, version : String) : void` 187 | 188 | Write the XML Declaration. 189 | 190 | ### writeStartElement 191 | 192 | **Signature:** `writeStartElement(localName : String) : void` 193 | 194 | Writes a start tag to the output. 195 | 196 | ### writeStartElement 197 | 198 | **Signature:** `writeStartElement(namespaceURI : String, localName : String) : void` 199 | 200 | Writes a start tag to the output. 201 | 202 | ### writeStartElement 203 | 204 | **Signature:** `writeStartElement(prefix : String, localName : String, namespaceURI : String) : void` 205 | 206 | Writes a start tag to the output. 207 | 208 | ## Constructor Detail 209 | 210 | ## Method Detail 211 | 212 | ## Method Details 213 | 214 | ### close 215 | 216 | **Signature:** `close() : void` 217 | 218 | **Description:** Close this writer and free any resources associated with the writer. This method does not close the underlying writer. 219 | 220 | --- 221 | 222 | ### flush 223 | 224 | **Signature:** `flush() : void` 225 | 226 | **Description:** Write any cached data to the underlying output mechanism. 227 | 228 | --- 229 | 230 | ### getDefaultNamespace 231 | 232 | **Signature:** `getDefaultNamespace() : String` 233 | 234 | **Description:** Returns the current default name space. 235 | 236 | **Returns:** 237 | 238 | the current default name space. 239 | 240 | --- 241 | 242 | ### getPrefix 243 | 244 | **Signature:** `getPrefix(uri : String) : String` 245 | 246 | **Description:** Gets the prefix the URI is bound to. 247 | 248 | **Parameters:** 249 | 250 | - `uri`: the URI to use. 251 | 252 | **Returns:** 253 | 254 | the prefix or null. 255 | 256 | --- 257 | 258 | ### setDefaultNamespace 259 | 260 | **Signature:** `setDefaultNamespace(uri : String) : void` 261 | 262 | **Description:** Binds a URI to the default namespace. This URI is bound in the scope of the current START_ELEMENT / END_ELEMENT pair. If this method is called before a START_ELEMENT has been written the uri is bound in the root scope. 263 | 264 | **Parameters:** 265 | 266 | - `uri`: the uri to bind to the default namespace, may be null. 267 | 268 | --- 269 | 270 | ### setPrefix 271 | 272 | **Signature:** `setPrefix(prefix : String, uri : String) : void` 273 | 274 | **Description:** Sets the prefix the uri is bound to. This prefix is bound in the scope of the current START_ELEMENT / END_ELEMENT pair. If this method is called before a START_ELEMENT has been written the prefix is bound in the root scope. 275 | 276 | **Parameters:** 277 | 278 | - `prefix`: the prefix to bind to the uri, may not be null. 279 | - `uri`: the uri to bind to the prefix, may be null. 280 | 281 | --- 282 | 283 | ### writeAttribute 284 | 285 | **Signature:** `writeAttribute(localName : String, value : String) : void` 286 | 287 | **Description:** Writes an attribute to the output stream without a prefix. 288 | 289 | **Parameters:** 290 | 291 | - `localName`: the local name of the attribute. 292 | - `value`: the value of the attribute. 293 | 294 | --- 295 | 296 | ### writeAttribute 297 | 298 | **Signature:** `writeAttribute(prefix : String, namespaceURI : String, localName : String, value : String) : void` 299 | 300 | **Description:** Writes an attribute to the output stream. 301 | 302 | **Parameters:** 303 | 304 | - `prefix`: the prefix for this attribute. 305 | - `namespaceURI`: the uri of the prefix for this attribute. 306 | - `localName`: the local name of the attribute. 307 | - `value`: the value of the attribute. 308 | 309 | --- 310 | 311 | ### writeAttribute 312 | 313 | **Signature:** `writeAttribute(namespaceURI : String, localName : String, value : String) : void` 314 | 315 | **Description:** Writes an attribute to the output stream. 316 | 317 | **Parameters:** 318 | 319 | - `namespaceURI`: the uri of the prefix for this attribute. 320 | - `localName`: the local name of the attribute. 321 | - `value`: the value of the attribute. 322 | 323 | --- 324 | 325 | ### writeCData 326 | 327 | **Signature:** `writeCData(data : String) : void` 328 | 329 | **Description:** Writes a CData section. 330 | 331 | **Parameters:** 332 | 333 | - `data`: the data contained in the CData Section, may not be null. 334 | 335 | --- 336 | 337 | ### writeCharacters 338 | 339 | **Signature:** `writeCharacters(text : String) : void` 340 | 341 | **Description:** Write text to the output. 342 | 343 | **Parameters:** 344 | 345 | - `text`: the value to write. 346 | 347 | --- 348 | 349 | ### writeComment 350 | 351 | **Signature:** `writeComment(data : String) : void` 352 | 353 | **Description:** Writes an XML comment with the data enclosed. 354 | 355 | **Parameters:** 356 | 357 | - `data`: the data contained in the comment, may be null. 358 | 359 | --- 360 | 361 | ### writeDefaultNamespace 362 | 363 | **Signature:** `writeDefaultNamespace(namespaceURI : String) : void` 364 | 365 | **Description:** Writes the default namespace to the stream. 366 | 367 | **Parameters:** 368 | 369 | - `namespaceURI`: the uri to bind the default namespace to. 370 | 371 | --- 372 | 373 | ### writeDTD 374 | 375 | **Signature:** `writeDTD(dtd : String) : void` 376 | 377 | **Description:** Write a DTD section. This string represents the entire doctypedecl production from the XML 1.0 specification. 378 | 379 | **Parameters:** 380 | 381 | - `dtd`: the DTD to be written. 382 | 383 | --- 384 | 385 | ### writeEmptyElement 386 | 387 | **Signature:** `writeEmptyElement(namespaceURI : String, localName : String) : void` 388 | 389 | **Description:** Writes an empty element tag to the output. 390 | 391 | **Parameters:** 392 | 393 | - `namespaceURI`: the uri to bind the tag to, may not be null. 394 | - `localName`: local name of the tag, may not be null. 395 | 396 | --- 397 | 398 | ### writeEmptyElement 399 | 400 | **Signature:** `writeEmptyElement(prefix : String, localName : String, namespaceURI : String) : void` 401 | 402 | **Description:** Writes an empty element tag to the output. 403 | 404 | **Parameters:** 405 | 406 | - `prefix`: the prefix of the tag, may not be null. 407 | - `localName`: local name of the tag, may not be null. 408 | - `namespaceURI`: the uri to bind the tag to, may not be null. 409 | 410 | --- 411 | 412 | ### writeEmptyElement 413 | 414 | **Signature:** `writeEmptyElement(localName : String) : void` 415 | 416 | **Description:** Writes an empty element tag to the output. 417 | 418 | **Parameters:** 419 | 420 | - `localName`: local name of the tag, may not be null. 421 | 422 | --- 423 | 424 | ### writeEndDocument 425 | 426 | **Signature:** `writeEndDocument() : void` 427 | 428 | **Description:** Closes any start tags and writes corresponding end tags. 429 | 430 | --- 431 | 432 | ### writeEndElement 433 | 434 | **Signature:** `writeEndElement() : void` 435 | 436 | **Description:** Writes an end tag to the output relying on the internal state of the writer to determine the prefix and local name of the event. 437 | 438 | --- 439 | 440 | ### writeEntityRef 441 | 442 | **Signature:** `writeEntityRef(name : String) : void` 443 | 444 | **Description:** Writes an entity reference. 445 | 446 | **Parameters:** 447 | 448 | - `name`: the name of the entity. 449 | 450 | --- 451 | 452 | ### writeNamespace 453 | 454 | **Signature:** `writeNamespace(prefix : String, namespaceURI : String) : void` 455 | 456 | **Description:** Writes a namespace to the output stream. If the prefix argument to this method is the empty string, "xmlns", or null this method will delegate to writeDefaultNamespace. 457 | 458 | **Parameters:** 459 | 460 | - `prefix`: the prefix to bind this namespace to. 461 | - `namespaceURI`: the uri to bind the prefix to. 462 | 463 | --- 464 | 465 | ### writeProcessingInstruction 466 | 467 | **Signature:** `writeProcessingInstruction(target : String) : void` 468 | 469 | **Description:** Writes a processing instruction. 470 | 471 | **Parameters:** 472 | 473 | - `target`: the target of the processing instruction, may not be null. 474 | 475 | --- 476 | 477 | ### writeProcessingInstruction 478 | 479 | **Signature:** `writeProcessingInstruction(target : String, data : String) : void` 480 | 481 | **Description:** Writes a processing instruction. 482 | 483 | **Parameters:** 484 | 485 | - `target`: the target of the processing instruction, may not be null. 486 | - `data`: the data contained in the processing instruction, may not be null. 487 | 488 | --- 489 | 490 | ### writeRaw 491 | 492 | **Signature:** `writeRaw(raw : String) : void` 493 | 494 | **Description:** Writes the given string directly into the output stream. No checks regarding the correctness of the XML are done. The caller must ensure that the final result is a correct XML. 495 | 496 | **Parameters:** 497 | 498 | - `raw`: the string to write to the output stream. 499 | 500 | --- 501 | 502 | ### writeStartDocument 503 | 504 | **Signature:** `writeStartDocument() : void` 505 | 506 | **Description:** Write the XML Declaration. Defaults the XML version to 1.0, and the encoding to utf-8 507 | 508 | --- 509 | 510 | ### writeStartDocument 511 | 512 | **Signature:** `writeStartDocument(version : String) : void` 513 | 514 | **Description:** Write the XML Declaration. Defaults the XML version to 1.0 515 | 516 | **Parameters:** 517 | 518 | - `version`: version of the xml document. 519 | 520 | --- 521 | 522 | ### writeStartDocument 523 | 524 | **Signature:** `writeStartDocument(encoding : String, version : String) : void` 525 | 526 | **Description:** Write the XML Declaration. Note that the encoding parameter does not set the actual encoding of the underlying output. That must be set when the instance of the XMLStreamWriter is created using the XMLOutputFactory. 527 | 528 | **Parameters:** 529 | 530 | - `encoding`: encoding of the xml declaration. 531 | - `version`: version of the xml document. 532 | 533 | --- 534 | 535 | ### writeStartElement 536 | 537 | **Signature:** `writeStartElement(localName : String) : void` 538 | 539 | **Description:** Writes a start tag to the output. All writeStartElement methods open a new scope in the internal namespace context. Writing the corresponding EndElement causes the scope to be closed. 540 | 541 | **Parameters:** 542 | 543 | - `localName`: local name of the tag, may not be null. 544 | 545 | --- 546 | 547 | ### writeStartElement 548 | 549 | **Signature:** `writeStartElement(namespaceURI : String, localName : String) : void` 550 | 551 | **Description:** Writes a start tag to the output. 552 | 553 | **Parameters:** 554 | 555 | - `namespaceURI`: the namespaceURI of the prefix to use, may not be null. 556 | - `localName`: local name of the tag, may not be null. 557 | 558 | --- 559 | 560 | ### writeStartElement 561 | 562 | **Signature:** `writeStartElement(prefix : String, localName : String, namespaceURI : String) : void` 563 | 564 | **Description:** Writes a start tag to the output. 565 | 566 | **Parameters:** 567 | 568 | - `prefix`: the prefix of the tag, may not be null. 569 | - `localName`: local name of the tag, may not be null. 570 | - `namespaceURI`: the uri to bind the prefix to, may not be null. 571 | 572 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/node/summarize-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('summarize_logs - Full Mode Programmatic Tests', () => { 6 | let client; 7 | 8 | before(async () => { 9 | client = await connect('./aegis.config.with-dw.json'); 10 | }); 11 | 12 | after(async () => { 13 | if (client?.connected) { 14 | await client.disconnect(); 15 | } 16 | }); 17 | 18 | beforeEach(() => { 19 | // CRITICAL: Clear all buffers to prevent leaking into next tests 20 | client.clearAllBuffers(); // Recommended - comprehensive protection 21 | }); 22 | 23 | // Helper function to get current date in YYYYMMDD format 24 | function getCurrentDateString() { 25 | const now = new Date(); 26 | const year = now.getFullYear(); 27 | const month = String(now.getMonth() + 1).padStart(2, '0'); 28 | const day = String(now.getDate()).padStart(2, '0'); 29 | return `${year}${month}${day}`; 30 | } 31 | 32 | // Comprehensive helper functions for complex validation 33 | function assertValidMCPResponse(result) { 34 | assert.ok(result.content, 'Should have content'); 35 | assert.ok(Array.isArray(result.content), 'Content should be array'); 36 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); 37 | } 38 | 39 | function assertSuccessResponse(result) { 40 | assertValidMCPResponse(result); 41 | assert.equal(result.isError, false, 'Should not be an error response'); 42 | assert.equal(result.content[0].type, 'text'); 43 | } 44 | 45 | function validateCompleteLogSummary(result, expectedDate = null) { 46 | assertSuccessResponse(result); 47 | const text = result.content[0].text; 48 | 49 | // Validate header format 50 | if (expectedDate) { 51 | assert.ok(text.includes(`Log Summary for ${expectedDate}`), 52 | `Should contain "Log Summary for ${expectedDate}"`); 53 | } else { 54 | assert.ok(/Log Summary for \d{8}/.test(text), 55 | 'Should contain "Log Summary for YYYYMMDD" pattern'); 56 | } 57 | 58 | // Validate all required sections with complex logic 59 | const requiredSections = ['📊 Counts:', '📁 Log Files', '🔥 Key Issues:']; 60 | const missingSections = requiredSections.filter(section => !text.includes(section)); 61 | assert.equal(missingSections.length, 0, 62 | `Missing required sections: ${missingSections.join(', ')}`); 63 | 64 | // Validate numeric count patterns with regex precision 65 | const countPatterns = [ 66 | { pattern: /- Errors: \d+/, name: 'Errors' }, 67 | { pattern: /- Warnings: \d+/, name: 'Warnings' }, 68 | { pattern: /- Info: \d+/, name: 'Info' }, 69 | { pattern: /- Debug: \d+/, name: 'Debug' } 70 | ]; 71 | 72 | const failedCounts = countPatterns.filter(({ pattern, name }) => { 73 | const matches = pattern.test(text); 74 | if (!matches) { 75 | console.log(`Failed count validation for ${name}: pattern ${pattern} not found in text`); 76 | } 77 | return !matches; 78 | }); 79 | 80 | assert.equal(failedCounts.length, 0, 81 | `Failed count validations: ${failedCounts.map(f => f.name).join(', ')}`); 82 | 83 | return { text, sections: requiredSections }; 84 | } 85 | 86 | function validateLogFileSection(text) { 87 | // Complex validation for log file section format 88 | const logFileMatch = text.match(/📁 Log Files \((\d+)\):/); 89 | if (logFileMatch) { 90 | const fileCount = parseInt(logFileMatch[1], 10); 91 | 92 | // Count actual log file entries 93 | const logFilePatterns = [ 94 | /error-blade-\d{8}-\d{6}\.log/g, 95 | /warn-blade-\d{8}-\d{6}\.log/g, 96 | /info-blade-\d{8}-\d{6}\.log/g, 97 | /debug-blade-\d{8}-\d{6}\.log/g 98 | ]; 99 | 100 | let actualFileCount = 0; 101 | logFilePatterns.forEach(pattern => { 102 | const matches = text.match(pattern) || []; 103 | actualFileCount += matches.length; 104 | }); 105 | 106 | return { declaredCount: fileCount, actualCount: actualFileCount }; 107 | } 108 | return null; 109 | } 110 | 111 | function validateKeyIssuesSection(text) { 112 | // Extract and validate key issues content 113 | const keyIssuesSplit = text.split('🔥 Key Issues:'); 114 | if (keyIssuesSplit.length > 1) { 115 | const keyIssuesContent = keyIssuesSplit[1].trim(); 116 | 117 | // Should not be empty 118 | assert.ok(keyIssuesContent.length > 0, 'Key issues section should not be empty'); 119 | 120 | // Should contain meaningful assessment patterns 121 | const assessmentPatterns = [ 122 | /No major issues detected/i, 123 | /Key issues identified/i, 124 | /Critical errors found/i, 125 | /\d+ error/i, 126 | /\d+ warning/i, 127 | /detected/i, 128 | /found/i 129 | ]; 130 | 131 | const hasValidAssessment = assessmentPatterns.some(pattern => pattern.test(keyIssuesContent)); 132 | assert.ok(hasValidAssessment, 133 | `Key issues should contain meaningful assessment. Content: "${keyIssuesContent}"`); 134 | 135 | return keyIssuesContent; 136 | } 137 | return null; 138 | } 139 | 140 | // Core functionality - comprehensive validation 141 | describe('Comprehensive Functionality Validation', () => { 142 | test('should provide complete log summary with all sections and cross-validation', async () => { 143 | const result = await client.callTool('summarize_logs', {}); 144 | const validation = validateCompleteLogSummary(result); 145 | 146 | // Advanced cross-section validation 147 | const fileValidation = validateLogFileSection(validation.text); 148 | if (fileValidation) { 149 | // Log file count should be reasonable (not negative, not excessively high) 150 | assert.ok(fileValidation.declaredCount >= 0, 'File count should not be negative'); 151 | assert.ok(fileValidation.declaredCount <= 100, 'File count should be reasonable (<=100)'); 152 | 153 | // If we have specific file entries, count should match or be reasonable 154 | if (fileValidation.actualCount > 0) { 155 | assert.ok(fileValidation.declaredCount >= fileValidation.actualCount, 156 | `Declared count (${fileValidation.declaredCount}) should be >= actual entries (${fileValidation.actualCount})`); 157 | } 158 | } 159 | 160 | // Validate key issues assessment quality 161 | const keyIssuesContent = validateKeyIssuesSection(validation.text); 162 | assert.ok(keyIssuesContent, 'Should have key issues section'); 163 | }); 164 | 165 | test('should handle both log scenarios with appropriate responses', async () => { 166 | const result = await client.callTool('summarize_logs', {}); 167 | assertValidMCPResponse(result); 168 | 169 | const text = result.content[0].text; 170 | const hasLogSummary = text.includes('📊 Counts:'); 171 | const noLogsFound = text.includes('No log files found'); 172 | 173 | assert.ok(hasLogSummary || noLogsFound, 174 | 'Should either show log summary or no logs message'); 175 | 176 | if (hasLogSummary) { 177 | // Comprehensive validation for log summary scenario 178 | validateCompleteLogSummary(result); 179 | } else { 180 | // Validation for no logs scenario 181 | assert.ok(/No log files found for date \d{8}/.test(text), 182 | 'No logs message should include specific date in YYYYMMDD format'); 183 | } 184 | }); 185 | }); 186 | 187 | // Dynamic date validation with cross-scenario consistency 188 | describe('Dynamic Date Validation and Consistency', () => { 189 | test('should maintain consistent response format across multiple date scenarios', async () => { 190 | const testScenarios = [ 191 | { date: getCurrentDateString(), description: 'current date' }, 192 | { date: '20220101', description: 'old date' }, 193 | { date: '20301231', description: 'future date' }, 194 | { date: 'invalid-format', description: 'invalid format' }, 195 | { date: '2024-01-01', description: 'wrong format' }, 196 | { date: '', description: 'empty string' } 197 | ]; 198 | 199 | const results = []; 200 | 201 | // Sequential execution to avoid buffer conflicts 202 | for (const scenario of testScenarios) { 203 | const result = await client.callTool('summarize_logs', { date: scenario.date }); 204 | assertValidMCPResponse(result); 205 | 206 | results.push({ 207 | ...scenario, 208 | result, 209 | text: result.content[0].text, 210 | isError: result.isError, 211 | hasLogSummary: result.content[0].text.includes('📊 Counts:'), 212 | isNoLogsMessage: result.content[0].text.includes('No log files found') 213 | }); 214 | } 215 | 216 | // Cross-scenario validation 217 | results.forEach(({ date, description, result, text, isError, hasLogSummary, isNoLogsMessage }) => { 218 | // All should follow same response structure 219 | assert.equal(result.content.length, 1, 220 | `${description} should have exactly one content element`); 221 | assert.equal(result.content[0].type, 'text', 222 | `${description} content type should be text`); 223 | assert.equal(isError, false, 224 | `${description} should not be marked as error`); 225 | 226 | // Should either have log summary or no logs message 227 | assert.ok(hasLogSummary || isNoLogsMessage, 228 | `${description} should either show summary or no logs message`); 229 | 230 | // Date format validation in response 231 | if (hasLogSummary) { 232 | if (date === '') { 233 | // Empty string date results in "Log Summary for :" format 234 | assert.ok(text.includes('Log Summary for :'), 235 | `${description} with empty date should include "Log Summary for :"`); 236 | } else { 237 | assert.ok(/Log Summary for \d{8}/.test(text), 238 | `${description} summary should include YYYYMMDD date`); 239 | } 240 | } else if (isNoLogsMessage) { 241 | assert.ok(text.includes(`No log files found for date ${date}`), 242 | `${description} should include the exact date parameter in no-logs message`); 243 | } 244 | }); 245 | 246 | // Validate consistent behavior for invalid dates 247 | const invalidDateResults = results.filter(r => 248 | ['invalid-format', 'wrong format'].includes(r.description)); // Removed 'empty string' since it acts like default 249 | 250 | invalidDateResults.forEach(({ description, isNoLogsMessage }) => { 251 | assert.ok(isNoLogsMessage, 252 | `${description} should result in no logs message`); 253 | }); 254 | }); 255 | 256 | test('should handle parameter variations and edge cases dynamically', async () => { 257 | const parameterVariations = [ 258 | { args: {}, description: 'empty object' }, 259 | { args: undefined, description: 'undefined args' }, 260 | { args: { date: null }, description: 'null date' }, 261 | { args: { date: getCurrentDateString(), extra: 'ignored' }, description: 'extra parameters' } 262 | ]; 263 | 264 | for (const { args, description } of parameterVariations) { 265 | const result = await client.callTool('summarize_logs', args); 266 | assertValidMCPResponse(result); 267 | 268 | // Should handle gracefully without errors 269 | assert.equal(result.isError, false, 270 | `${description} should not cause error response`); 271 | 272 | const text = result.content[0].text; 273 | const hasValidResponse = text.includes('📊 Counts:') || 274 | text.includes('No log files found'); 275 | 276 | assert.ok(hasValidResponse, 277 | `${description} should produce valid response format`); 278 | } 279 | }); 280 | }); 281 | 282 | // Advanced content analysis and business logic validation 283 | describe('Advanced Content Analysis', () => { 284 | test('should validate log count arithmetic and consistency', async () => { 285 | const result = await client.callTool('summarize_logs', {}); 286 | assertSuccessResponse(result); 287 | 288 | const text = result.content[0].text; 289 | 290 | if (text.includes('📊 Counts:')) { 291 | // Extract all numeric counts 292 | const countMatches = { 293 | errors: text.match(/- Errors: (\d+)/), 294 | warnings: text.match(/- Warnings: (\d+)/), 295 | info: text.match(/- Info: (\d+)/), 296 | debug: text.match(/- Debug: (\d+)/) 297 | }; 298 | 299 | // Validate all counts were found 300 | Object.entries(countMatches).forEach(([level, match]) => { 301 | assert.ok(match, `Should find ${level} count in response`); 302 | 303 | const count = parseInt(match[1], 10); 304 | assert.ok(count >= 0, `${level} count should not be negative`); 305 | assert.ok(count < 1000000, `${level} count should be reasonable (<1M)`); 306 | }); 307 | 308 | // Advanced business logic: total log entries should be reasonable 309 | const totalEntries = Object.values(countMatches) 310 | .map(match => parseInt(match[1], 10)) 311 | .reduce((sum, count) => sum + count, 0); 312 | 313 | assert.ok(totalEntries >= 0, 'Total log entries should not be negative'); 314 | 315 | // If we have log entries, we should have log files 316 | if (totalEntries > 0) { 317 | assert.ok(text.includes('📁 Log Files'), 318 | 'Should have log files section when log entries exist'); 319 | } 320 | } 321 | }); 322 | 323 | test('should validate emoji consistency and section ordering', async () => { 324 | const result = await client.callTool('summarize_logs', {}); 325 | assertSuccessResponse(result); 326 | 327 | const text = result.content[0].text; 328 | 329 | if (text.includes('📊 Counts:')) { 330 | // Validate section ordering by finding their positions 331 | const sectionPositions = { 332 | counts: text.indexOf('📊 Counts:'), 333 | files: text.indexOf('📁 Log Files'), 334 | issues: text.indexOf('🔥 Key Issues:') 335 | }; 336 | 337 | // All sections should be found 338 | Object.entries(sectionPositions).forEach(([section, position]) => { 339 | assert.ok(position >= 0, `Should find ${section} section`); 340 | }); 341 | 342 | // Validate logical ordering: counts -> files -> issues 343 | assert.ok(sectionPositions.counts < sectionPositions.files, 344 | 'Counts section should come before files section'); 345 | assert.ok(sectionPositions.files < sectionPositions.issues, 346 | 'Files section should come before issues section'); 347 | 348 | // Validate consistent emoji usage 349 | const expectedEmojis = ['📊', '📁', '🔥']; 350 | expectedEmojis.forEach(emoji => { 351 | assert.ok(text.includes(emoji), 352 | `Should use ${emoji} emoji consistently`); 353 | }); 354 | } 355 | }); 356 | }); 357 | }); 358 | ``` -------------------------------------------------------------------------------- /docs/dw_suggest/SuggestModel.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.suggest 2 | 3 | # Class SuggestModel 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.suggest.SuggestModel 9 | 10 | ## Description 11 | 12 | The Suggest model provides methods and functions to access search suggestions. The search suggestion feature basically covers two functional areas. First is just to suggest words, based on the users input, utilizing spell correction or prediction (also known as auto completion). The second functional area is also often referred to as search-as-you-type, where, based on the users input, specific items are already looked up, before the user actually has completed typing a word or even fired up the search. This model combines both functional areas and provides access to both - the suggested words and the items found while using the predicted words. This model supports various types of items that are being suggested, like products, categories, brands, content pages as well merchant provided search phrases. For each type, there is a Suggestions implementation available and accessible through this model: ProductSuggestions, CategorySuggestions, BrandSuggestions, ContentSuggestions, and CustomSuggestions. For each type of suggestions, the actual suggested items (like products) can by obtained, and, on the other hand, a list of terms is provided which were used to lookup the found items. The terms can be used to present a advanced user experience in the storefront, e.g. show auto completed words, spell corrections and so on. The SuggestModel script API will always create suggestions with Autocorrections regardless of the value of "Search Autocorrections" search preference. 13 | 14 | ## Constants 15 | 16 | ### MAX_SUGGESTIONS 17 | 18 | **Type:** Number = 10 19 | 20 | The maximum number of suggestions that can be obtain from this model: 10 21 | 22 | ## Properties 23 | 24 | ### brandSuggestions 25 | 26 | **Type:** BrandSuggestions (Read Only) 27 | 28 | A BrandSuggestions container for the current search phrase. 29 | The BrandSuggestions container provides access to the found brands (if any) and 30 | the terms suggested by the system with respect to the known product brands in the catalog. 31 | 32 | ### categorySuggestions 33 | 34 | **Type:** CategorySuggestions (Read Only) 35 | 36 | A CategorySuggestions container for the current search phrase. 37 | The CategorySuggestions container provides access to the found categories (if any) and 38 | the terms suggested by the system with respect to the known categories in the catalog. 39 | 40 | ### contentSuggestions 41 | 42 | **Type:** ContentSuggestions (Read Only) 43 | 44 | A ContentSuggestions container for the current search phrase. 45 | The ContentSuggestions container provides access to the found content pages (if any) and 46 | the terms suggested by the system with respect to the known content in the library. 47 | 48 | ### customSuggestions 49 | 50 | **Type:** CustomSuggestions (Read Only) 51 | 52 | A CustomSuggestions container for the current search phrase. 53 | The CustomSuggestions container provides access to matching 54 | custom phrases (if any) and the terms suggested 55 | by the system with respect to the merchant provided custom phrases. 56 | 57 | ### filteredByFolder 58 | 59 | **Type:** boolean 60 | 61 | The method returns true, if the search suggestions are filtered by the folder. If this returns true it is not 62 | possible for search suggestions to contain Page Designer content as it belongs to no folder. 63 | 64 | ### popularSearchPhrases 65 | 66 | **Type:** Iterator (Read Only) 67 | 68 | Use this method to obtain a list of search phrases 69 | that currently are very popular among all users across the Site. 70 | 71 | The search phrases are specific to the region (based on user's IP address), 72 | language (locale) and the user's browser type (agent). 73 | 74 | ### productSuggestions 75 | 76 | **Type:** ProductSuggestions (Read Only) 77 | 78 | A ProductSuggestions container for the current search phrase. 79 | The ProductSuggestions container provides access to the found products (if any) and 80 | the terms suggested by the system with respect to the known products in the catalog. 81 | 82 | ### recentSearchPhrases 83 | 84 | **Type:** Iterator (Read Only) 85 | 86 | Use this method to obtain a list of personalized search phrases 87 | that the current user entered recently. 88 | 89 | The user is being identified by the CQuotient tracking cookie. 90 | 91 | ## Constructor Summary 92 | 93 | SuggestModel() Constructs a new SuggestModel. 94 | 95 | ## Method Summary 96 | 97 | ### addRefinementValues 98 | 99 | **Signature:** `addRefinementValues(attributeID : String, values : String) : void` 100 | 101 | Adds a refinement for product suggestions. 102 | 103 | ### getBrandSuggestions 104 | 105 | **Signature:** `getBrandSuggestions() : BrandSuggestions` 106 | 107 | Returns a BrandSuggestions container for the current search phrase. 108 | 109 | ### getCategorySuggestions 110 | 111 | **Signature:** `getCategorySuggestions() : CategorySuggestions` 112 | 113 | Returns a CategorySuggestions container for the current search phrase. 114 | 115 | ### getContentSuggestions 116 | 117 | **Signature:** `getContentSuggestions() : ContentSuggestions` 118 | 119 | Returns a ContentSuggestions container for the current search phrase. 120 | 121 | ### getCustomSuggestions 122 | 123 | **Signature:** `getCustomSuggestions() : CustomSuggestions` 124 | 125 | Returns a CustomSuggestions container for the current search phrase. 126 | 127 | ### getPopularSearchPhrases 128 | 129 | **Signature:** `getPopularSearchPhrases() : Iterator` 130 | 131 | Use this method to obtain a list of search phrases that currently are very popular among all users across the Site. 132 | 133 | ### getProductSuggestions 134 | 135 | **Signature:** `getProductSuggestions() : ProductSuggestions` 136 | 137 | Returns a ProductSuggestions container for the current search phrase. 138 | 139 | ### getRecentSearchPhrases 140 | 141 | **Signature:** `getRecentSearchPhrases() : Iterator` 142 | 143 | Use this method to obtain a list of personalized search phrases that the current user entered recently. 144 | 145 | ### isFilteredByFolder 146 | 147 | **Signature:** `isFilteredByFolder() : boolean` 148 | 149 | The method returns true, if the search suggestions are filtered by the folder. 150 | 151 | ### removeRefinementValues 152 | 153 | **Signature:** `removeRefinementValues(attributeID : String, values : String) : void` 154 | 155 | Removes a refinement. 156 | 157 | ### setCategoryID 158 | 159 | **Signature:** `setCategoryID(categoryID : String) : void` 160 | 161 | Apply a category ID to filter product, brand and category suggestions. 162 | 163 | ### setFilteredByFolder 164 | 165 | **Signature:** `setFilteredByFolder(filteredByFolder : boolean) : void` 166 | 167 | Set a flag to indicate if the search suggestions filter for elements that do not belong to a folder. 168 | 169 | ### setMaxSuggestions 170 | 171 | **Signature:** `setMaxSuggestions(maxSuggestions : Number) : void` 172 | 173 | Use this method to setup the maximum number of returned suggested items. 174 | 175 | ### setRefinementValues 176 | 177 | **Signature:** `setRefinementValues(attributeID : String, values : String) : void` 178 | 179 | Sets product suggestion refinement values for an attribute. 180 | 181 | ### setSearchPhrase 182 | 183 | **Signature:** `setSearchPhrase(searchPhrase : String) : void` 184 | 185 | Sets the user input search phrase. 186 | 187 | ## Constructor Detail 188 | 189 | ## Method Detail 190 | 191 | ## Method Details 192 | 193 | ### addRefinementValues 194 | 195 | **Signature:** `addRefinementValues(attributeID : String, values : String) : void` 196 | 197 | **Description:** Adds a refinement for product suggestions. The method can be called to add an additional query parameter specified as name-value pair. The values string may encode multiple values delimited by the pipe symbol ('|'). 198 | 199 | **Parameters:** 200 | 201 | - `attributeID`: The ID of the refinement attribute. 202 | - `values`: the refinement value to set 203 | 204 | --- 205 | 206 | ### getBrandSuggestions 207 | 208 | **Signature:** `getBrandSuggestions() : BrandSuggestions` 209 | 210 | **Description:** Returns a BrandSuggestions container for the current search phrase. The BrandSuggestions container provides access to the found brands (if any) and the terms suggested by the system with respect to the known product brands in the catalog. 211 | 212 | **Returns:** 213 | 214 | a brand suggestions container for the current search phrase, returns null for insufficient search input 215 | 216 | **See Also:** 217 | 218 | setMaxSuggestions(Number) 219 | setSearchPhrase(String) 220 | 221 | --- 222 | 223 | ### getCategorySuggestions 224 | 225 | **Signature:** `getCategorySuggestions() : CategorySuggestions` 226 | 227 | **Description:** Returns a CategorySuggestions container for the current search phrase. The CategorySuggestions container provides access to the found categories (if any) and the terms suggested by the system with respect to the known categories in the catalog. 228 | 229 | **Returns:** 230 | 231 | a category suggestions container for the current search phrase, returns null for insufficient search input 232 | 233 | **See Also:** 234 | 235 | setMaxSuggestions(Number) 236 | setSearchPhrase(String) 237 | 238 | --- 239 | 240 | ### getContentSuggestions 241 | 242 | **Signature:** `getContentSuggestions() : ContentSuggestions` 243 | 244 | **Description:** Returns a ContentSuggestions container for the current search phrase. The ContentSuggestions container provides access to the found content pages (if any) and the terms suggested by the system with respect to the known content in the library. 245 | 246 | **Returns:** 247 | 248 | a content suggestions container for the current search phrase, returns null for insufficient search input 249 | 250 | **See Also:** 251 | 252 | setMaxSuggestions(Number) 253 | setSearchPhrase(String) 254 | 255 | --- 256 | 257 | ### getCustomSuggestions 258 | 259 | **Signature:** `getCustomSuggestions() : CustomSuggestions` 260 | 261 | **Description:** Returns a CustomSuggestions container for the current search phrase. The CustomSuggestions container provides access to matching custom phrases (if any) and the terms suggested by the system with respect to the merchant provided custom phrases. 262 | 263 | **Returns:** 264 | 265 | a custom suggestions container for the current search phrase, returns null for insufficient search input 266 | 267 | **See Also:** 268 | 269 | setMaxSuggestions(Number) 270 | setSearchPhrase(String) 271 | 272 | --- 273 | 274 | ### getPopularSearchPhrases 275 | 276 | **Signature:** `getPopularSearchPhrases() : Iterator` 277 | 278 | **Description:** Use this method to obtain a list of search phrases that currently are very popular among all users across the Site. The search phrases are specific to the region (based on user's IP address), language (locale) and the user's browser type (agent). 279 | 280 | **Returns:** 281 | 282 | a list of personalized popular search phrases 283 | 284 | --- 285 | 286 | ### getProductSuggestions 287 | 288 | **Signature:** `getProductSuggestions() : ProductSuggestions` 289 | 290 | **Description:** Returns a ProductSuggestions container for the current search phrase. The ProductSuggestions container provides access to the found products (if any) and the terms suggested by the system with respect to the known products in the catalog. 291 | 292 | **Returns:** 293 | 294 | a product suggestions container for the current search phrase, returns null for insufficient search input 295 | 296 | **See Also:** 297 | 298 | setMaxSuggestions(Number) 299 | setSearchPhrase(String) 300 | 301 | --- 302 | 303 | ### getRecentSearchPhrases 304 | 305 | **Signature:** `getRecentSearchPhrases() : Iterator` 306 | 307 | **Description:** Use this method to obtain a list of personalized search phrases that the current user entered recently. The user is being identified by the CQuotient tracking cookie. 308 | 309 | **Returns:** 310 | 311 | a list of recent search phrases of the current user 312 | 313 | --- 314 | 315 | ### isFilteredByFolder 316 | 317 | **Signature:** `isFilteredByFolder() : boolean` 318 | 319 | **Description:** The method returns true, if the search suggestions are filtered by the folder. If this returns true it is not possible for search suggestions to contain Page Designer content as it belongs to no folder. 320 | 321 | **Returns:** 322 | 323 | True if search suggestions are filtered by the folder of the content asset. 324 | 325 | --- 326 | 327 | ### removeRefinementValues 328 | 329 | **Signature:** `removeRefinementValues(attributeID : String, values : String) : void` 330 | 331 | **Description:** Removes a refinement. The method can be called to remove previously added refinement values. The values string may encode multiple values delimited by the pipe symbol ('|'). 332 | 333 | **Parameters:** 334 | 335 | - `attributeID`: The ID of the refinement attribute. 336 | - `values`: the refinement value to remove or null to remove all values 337 | 338 | --- 339 | 340 | ### setCategoryID 341 | 342 | **Signature:** `setCategoryID(categoryID : String) : void` 343 | 344 | **Description:** Apply a category ID to filter product, brand and category suggestions. Suggested products, brands and categories, as well as corrected and completed terms are specific to the given category or one of it's sub categories. For example, in the specified category "television", the search term "pla" will be auto completed to "plasma" (instead of e.g. "player") and only televisions will be included in the list of suggested products. 345 | 346 | **Parameters:** 347 | 348 | - `categoryID`: the category to filter suggestions for 349 | 350 | --- 351 | 352 | ### setFilteredByFolder 353 | 354 | **Signature:** `setFilteredByFolder(filteredByFolder : boolean) : void` 355 | 356 | **Description:** Set a flag to indicate if the search suggestions filter for elements that do not belong to a folder. Must be set to false to return content assets that do not belong to any folder. 357 | 358 | **Parameters:** 359 | 360 | - `filteredByFolder`: filter the search suggestions by folder 361 | 362 | --- 363 | 364 | ### setMaxSuggestions 365 | 366 | **Signature:** `setMaxSuggestions(maxSuggestions : Number) : void` 367 | 368 | **Description:** Use this method to setup the maximum number of returned suggested items. For example, set this to 3 in order to only retrieve the 3 most relevant suggested products. The maximum number of suggestions that can be queried are defined as MAX_SUGGESTIONS. 369 | 370 | **Parameters:** 371 | 372 | - `maxSuggestions`: the number of suggested items to be returned by this model instance 373 | 374 | --- 375 | 376 | ### setRefinementValues 377 | 378 | **Signature:** `setRefinementValues(attributeID : String, values : String) : void` 379 | 380 | **Description:** Sets product suggestion refinement values for an attribute. The method can be called to set an additional query parameter specified as name-value pair. The value string may encode multiple values delimited by the pipe symbol ('|'). Existing refinement values for the attribute will be removed. 381 | 382 | **Parameters:** 383 | 384 | - `attributeID`: The ID of the refinement attribute. 385 | - `values`: the refinement values to set (delimited by '|') or null to remove all values 386 | 387 | --- 388 | 389 | ### setSearchPhrase 390 | 391 | **Signature:** `setSearchPhrase(searchPhrase : String) : void` 392 | 393 | **Description:** Sets the user input search phrase. This search phrase is being processed by applying auto completion, spell correction and enhancement with alternative similar search terms. The resulting search phrase is used to lookup the actual items, like products or categories (search-as-you-type). In order to access the processed terms, one can use the SearchPhraseSuggestions.getSuggestedTerms() method of each of the respective results returned by the methods in this model. 394 | 395 | **Parameters:** 396 | 397 | - `searchPhrase`: the user input search phrase 398 | 399 | **See Also:** 400 | 401 | SearchPhraseSuggestions.getSuggestedTerms() 402 | 403 | --- ```