This is page 33 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/node/get-job-log-entries.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_job_log_entries - Full Mode Programmatic Tests', () => { 6 | let client; 7 | let discoveredJobNames = []; 8 | 9 | before(async () => { 10 | client = await connect('./aegis.config.with-dw.json'); 11 | 12 | // Discover available job names for advanced testing 13 | await discoverJobNames(); 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(); // Recommended - comprehensive protection 25 | }); 26 | 27 | // Helper functions for common validations 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 | // The response may come wrapped in quotes, so parse if needed 36 | return text.startsWith('"') && text.endsWith('"') 37 | ? JSON.parse(text) 38 | : text; 39 | } 40 | 41 | function assertTextContent(result, expectedSubstring) { 42 | assertValidMCPResponse(result); 43 | assert.equal(result.content[0].type, 'text'); 44 | const actualText = parseResponseText(result.content[0].text); 45 | assert.ok(actualText.includes(expectedSubstring), 46 | `Expected "${expectedSubstring}" in "${actualText}"`); 47 | } 48 | 49 | function assertSuccessResponse(result) { 50 | assertValidMCPResponse(result); 51 | assert.equal(result.isError, false, 'Should not be an error response'); 52 | assert.equal(result.content[0].type, 'text'); 53 | } 54 | 55 | function assertErrorResponse(result, expectedErrorText) { 56 | assertValidMCPResponse(result); 57 | assert.equal(result.isError, true, 'Should be an error response'); 58 | assert.equal(result.content[0].type, 'text'); 59 | if (expectedErrorText) { 60 | assertTextContent(result, expectedErrorText); 61 | } 62 | } 63 | 64 | function assertJobLogEntriesFormat(result, expectedLimit, expectedLevel = 'all levels', jobName = null) { 65 | assertSuccessResponse(result); 66 | const text = parseResponseText(result.content[0].text); 67 | 68 | // Determine expected header pattern 69 | let expectedHeader; 70 | if (jobName) { 71 | expectedHeader = `Latest ${expectedLimit} ${expectedLevel} messages from job: ${jobName}:`; 72 | } else { 73 | expectedHeader = `Latest ${expectedLimit} ${expectedLevel} messages from latest jobs:`; 74 | } 75 | 76 | // Check if it's an empty result 77 | if (text.trim() === expectedHeader.trim() || text.includes('No job logs found')) { 78 | // Valid empty result case 79 | return { entryCount: 0, jobNames: [] }; 80 | } 81 | 82 | // Should start with the expected header 83 | assert.ok(text.includes(expectedHeader), 84 | `Should start with "${expectedHeader}"`); 85 | 86 | // Extract job log entries 87 | const entries = extractJobLogEntries(text); 88 | 89 | // Validate each entry has proper structure 90 | const jobNames = new Set(); 91 | for (const entry of entries) { 92 | // Each entry should have job name in brackets 93 | assert.ok(/^\[[\w]+\]/.test(entry.trim()), 94 | `Entry should start with job name in brackets: "${entry.substring(0, 50)}..."`); 95 | 96 | // Should contain timestamp in GMT format 97 | assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(entry), 98 | `Entry should contain GMT timestamp: "${entry.substring(0, 100)}..."`); 99 | 100 | // Should contain log level (unless filtered to specific level) 101 | if (expectedLevel === 'all levels') { 102 | assert.ok(/(ERROR|WARN|INFO|DEBUG)/.test(entry), 103 | `Entry should contain log level: "${entry.substring(0, 100)}..."`); 104 | } else { 105 | const levelToCheck = expectedLevel.replace(' levels', '').replace(' messages', ''); 106 | assert.ok(entry.includes(levelToCheck.toUpperCase()), 107 | `Entry should contain ${levelToCheck.toUpperCase()} level: "${entry.substring(0, 100)}..."`); 108 | } 109 | 110 | // Should contain SystemJobThread pattern 111 | assert.ok(/SystemJobThread/.test(entry), 112 | `Entry should contain SystemJobThread pattern: "${entry.substring(0, 100)}..."`); 113 | 114 | // Extract job name for validation 115 | const jobNameMatch = entry.match(/^\[(\w+)\]/); 116 | if (jobNameMatch) { 117 | jobNames.add(jobNameMatch[1]); 118 | } 119 | } 120 | 121 | // If job name filter is specified, all entries should be from that job 122 | if (jobName) { 123 | for (const extractedJobName of jobNames) { 124 | assert.equal(extractedJobName, jobName, 125 | `All entries should be from job "${jobName}", found "${extractedJobName}"`); 126 | } 127 | } 128 | 129 | // Number of entries should not exceed limit 130 | assert.ok(entries.length <= expectedLimit, 131 | `Number of entries ${entries.length} should not exceed limit ${expectedLimit}`); 132 | 133 | return { entryCount: entries.length, jobNames: Array.from(jobNames) }; 134 | } 135 | 136 | function extractJobLogEntries(text) { 137 | // Split by the separator and filter out header and empty lines 138 | const parts = text.split('---').map(part => part.trim()).filter(part => 139 | part && !part.includes('Latest') && !part.includes('messages from')); 140 | return parts; 141 | } 142 | 143 | function assertJobExecutionPatterns(result) { 144 | assertSuccessResponse(result); 145 | const text = parseResponseText(result.content[0].text); 146 | 147 | // Should contain job execution patterns 148 | const patterns = [ 149 | /Execution of job finished with status/, 150 | /Step \[[\w]+\] completed successfully/, 151 | /Executing step \[[\w]+\] for/, 152 | /Executing job \[[\w]+\]\[[\d]+\]/ 153 | ]; 154 | 155 | const foundPatterns = patterns.filter(pattern => pattern.test(text)); 156 | assert.ok(foundPatterns.length > 0, 157 | `Should contain at least one job execution pattern in: "${text.substring(0, 200)}..."`); 158 | } 159 | 160 | async function discoverJobNames() { 161 | try { 162 | const result = await client.callTool('get_job_log_entries', { limit: 20 }); 163 | if (!result.isError) { 164 | const text = parseResponseText(result.content[0].text); 165 | 166 | // Look for job names that appear at the start of log lines 167 | // Pattern: [JobName] [timestamp] LEVEL SystemJobThread... 168 | const logLinePattern = /\n\[(\w+)\] \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT\] \w+ SystemJobThread/g; 169 | const jobNameMatches = []; 170 | let match; 171 | 172 | while ((match = logLinePattern.exec(text)) !== null) { 173 | jobNameMatches.push(match[1]); 174 | } 175 | 176 | if (jobNameMatches.length > 0) { 177 | // Filter out obvious non-job names and get unique values 178 | const validJobNames = jobNameMatches 179 | .filter(name => 180 | name.length > 2 && // Must be more than 2 characters 181 | !['OK', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'Organization'].includes(name) && // Exclude status words 182 | !/Step$/.test(name) // Exclude step names ending with "Step" 183 | ); 184 | discoveredJobNames = [...new Set(validJobNames)]; 185 | } 186 | 187 | // Fallback: if no job names found, use known job names from mock data 188 | if (discoveredJobNames.length === 0) { 189 | discoveredJobNames = ['ProcessOrders', 'ImportCatalog']; 190 | } 191 | } 192 | } catch (error) { 193 | // Discovery is optional, continue with tests using fallback 194 | discoveredJobNames = ['ProcessOrders', 'ImportCatalog']; 195 | console.warn('Job name discovery failed, using fallback:', error.message); 196 | } 197 | } 198 | 199 | // Core functionality and parameter validation tests 200 | describe('Core Functionality', () => { 201 | test('should retrieve job log entries with default parameters', async () => { 202 | const result = await client.callTool('get_job_log_entries', {}); 203 | 204 | assertJobLogEntriesFormat(result, 50); // Default limit is 50 205 | assertJobExecutionPatterns(result); 206 | 207 | // Should contain SFCC job-specific patterns 208 | const text = parseResponseText(result.content[0].text); 209 | assert.ok(/SystemJobThread/.test(text), 210 | 'Should contain SystemJobThread patterns'); 211 | }); 212 | 213 | test('should respect limit parameter boundaries', async () => { 214 | // Test small limit 215 | const smallResult = await client.callTool('get_job_log_entries', { limit: 1 }); 216 | const smallAnalysis = assertJobLogEntriesFormat(smallResult, 1); 217 | assert.ok(smallAnalysis.entryCount <= 1, 'Should respect small limit'); 218 | 219 | // Test reasonable limit 220 | const mediumResult = await client.callTool('get_job_log_entries', { limit: 10 }); 221 | const mediumAnalysis = assertJobLogEntriesFormat(mediumResult, 10); 222 | assert.ok(mediumAnalysis.entryCount <= 10, 'Should respect medium limit'); 223 | }); 224 | 225 | test('should filter by log levels correctly', async () => { 226 | // Test representative log levels (not all - YAML tests cover others) 227 | const testCases = [ 228 | { level: 'error', expected: 'error' }, 229 | { level: 'info', expected: 'info' }, 230 | { level: 'all', expected: 'all levels' } 231 | ]; 232 | 233 | for (const testCase of testCases) { 234 | const result = await client.callTool('get_job_log_entries', { 235 | level: testCase.level, 236 | limit: 3 237 | }); 238 | 239 | assertJobLogEntriesFormat(result, 3, testCase.expected); 240 | assertTextContent(result, `Latest 3 ${testCase.expected} messages from latest jobs:`); 241 | } 242 | }); 243 | 244 | test('should filter by job name when specified', async () => { 245 | const result = await client.callTool('get_job_log_entries', { 246 | jobName: 'ProcessOrders', 247 | limit: 3 248 | }); 249 | 250 | assertJobLogEntriesFormat(result, 3, 'all levels', 'ProcessOrders'); 251 | assertTextContent(result, 'Latest 3 all levels messages from job: ProcessOrders:'); 252 | }); 253 | 254 | test('should combine parameters correctly', async () => { 255 | const result = await client.callTool('get_job_log_entries', { 256 | level: 'info', 257 | limit: 5, 258 | jobName: 'ImportCatalog' 259 | }); 260 | 261 | assertJobLogEntriesFormat(result, 5, 'info', 'ImportCatalog'); 262 | assertTextContent(result, 'Latest 5 info messages from job: ImportCatalog:'); 263 | }); 264 | }); 265 | 266 | // Content structure and format validation tests 267 | describe('Content Validation', () => { 268 | test('should maintain consistent job log entry structure', async () => { 269 | const result = await client.callTool('get_job_log_entries', { limit: 10 }); 270 | 271 | assertSuccessResponse(result); 272 | 273 | const text = parseResponseText(result.content[0].text); 274 | const entries = extractJobLogEntries(text); 275 | 276 | // Each entry should follow consistent structure (test first 3 entries) 277 | for (const entry of entries.slice(0, 3)) { 278 | // Should start with job name in brackets 279 | assert.ok(/^\[[\w]+\]/.test(entry.trim()), 280 | `Entry should start with job name: "${entry.substring(0, 100)}..."`); 281 | 282 | // Should contain timestamp 283 | assert.ok(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT/.test(entry), 284 | `Entry should contain timestamp: "${entry.substring(0, 100)}..."`); 285 | 286 | // Should contain log level 287 | assert.ok(/(ERROR|WARN|INFO|DEBUG)/.test(entry), 288 | `Entry should contain log level: "${entry.substring(0, 100)}..."`); 289 | 290 | // Should contain SystemJobThread with ID pattern 291 | assert.ok(/SystemJobThread\|\d+/.test(entry), 292 | `Entry should contain SystemJobThread with ID: "${entry.substring(0, 100)}..."`); 293 | } 294 | }); 295 | 296 | test('should include proper job execution details', async () => { 297 | const result = await client.callTool('get_job_log_entries', { limit: 10 }); 298 | 299 | assertJobExecutionPatterns(result); 300 | 301 | const text = parseResponseText(result.content[0].text); 302 | 303 | // Should contain organization references 304 | assert.ok(/\[Organization\]/.test(text), 305 | 'Should contain Organization references'); 306 | }); 307 | }); 308 | 309 | // Error handling and edge cases 310 | describe('Error Handling', () => { 311 | test('should handle invalid limit values', async () => { 312 | // Test zero limit 313 | const zeroResult = await client.callTool('get_job_log_entries', { limit: 0 }); 314 | assertErrorResponse(zeroResult, 'Invalid limit'); 315 | assertTextContent(zeroResult, 'Must be between 1 and 1000'); 316 | 317 | // Test negative limit 318 | const negativeResult = await client.callTool('get_job_log_entries', { limit: -5 }); 319 | assertErrorResponse(negativeResult, 'Invalid limit'); 320 | 321 | // Test extremely large limit 322 | const largeResult = await client.callTool('get_job_log_entries', { limit: 10000 }); 323 | assertErrorResponse(largeResult, 'Invalid limit'); 324 | assertTextContent(largeResult, 'Must be between 1 and 1000'); 325 | }); 326 | 327 | test('should handle invalid log level gracefully', async () => { 328 | const result = await client.callTool('get_job_log_entries', { 329 | level: 'invalid', 330 | limit: 5 331 | }); 332 | 333 | assertErrorResponse(result, 'Error'); 334 | }); 335 | 336 | test('should handle nonexistent job name gracefully', async () => { 337 | const result = await client.callTool('get_job_log_entries', { 338 | jobName: 'NonExistentJob', 339 | limit: 5 340 | }); 341 | 342 | assertSuccessResponse(result); 343 | assertTextContent(result, 'No job logs found for job name: NonExistentJob'); 344 | }); 345 | 346 | test('should handle edge case job names', async () => { 347 | // Empty job name should be treated as no filter 348 | const emptyResult = await client.callTool('get_job_log_entries', { 349 | jobName: '', 350 | limit: 3 351 | }); 352 | assertSuccessResponse(emptyResult); 353 | assertTextContent(emptyResult, 'Latest 3 all levels messages from latest jobs:'); 354 | 355 | // Special characters should be handled gracefully 356 | const specialResult = await client.callTool('get_job_log_entries', { 357 | jobName: 'Job@#$%', 358 | limit: 3 359 | }); 360 | assertSuccessResponse(specialResult); 361 | assert.ok(specialResult.content[0].text.includes('Job@#$%') || 362 | specialResult.content[0].text.includes('No job logs found'), 363 | 'Should handle special characters gracefully'); 364 | }); 365 | }); 366 | 367 | // Advanced workflows using discovered job names 368 | describe('Dynamic Job Discovery Workflows', () => { 369 | test('should discover and analyze job patterns dynamically', async () => { 370 | if (discoveredJobNames.length === 0) { 371 | console.warn('No job names discovered, skipping dynamic tests'); 372 | return; 373 | } 374 | 375 | // Test with discovered job names (limit to first 2 for efficiency) 376 | for (const jobName of discoveredJobNames.slice(0, 2)) { 377 | const result = await client.callTool('get_job_log_entries', { 378 | jobName, 379 | limit: 5 380 | }); 381 | 382 | if (!result.isError) { 383 | assertJobLogEntriesFormat(result, 5, 'all levels', jobName); 384 | assertTextContent(result, `Latest 5 all levels messages from job: ${jobName}:`); 385 | } 386 | } 387 | }); 388 | 389 | test('should support progressive filtering workflow', async () => { 390 | // Step 1: Start with broad search to discover available content 391 | const broadResult = await client.callTool('get_job_log_entries', { limit: 20 }); 392 | assertSuccessResponse(broadResult); 393 | 394 | // Step 2: Focus on error level to identify problem areas 395 | const errorResult = await client.callTool('get_job_log_entries', { 396 | level: 'error', 397 | limit: 5 398 | }); 399 | 400 | assertValidMCPResponse(errorResult); 401 | 402 | // Step 3: If errors found and job names discovered, drill down 403 | if (!errorResult.isError && discoveredJobNames.length > 0) { 404 | const jobName = discoveredJobNames[0]; 405 | const specificErrorResult = await client.callTool('get_job_log_entries', { 406 | jobName, 407 | level: 'error', 408 | limit: 3 409 | }); 410 | 411 | assertValidMCPResponse(specificErrorResult); 412 | if (!specificErrorResult.isError) { 413 | assertJobLogEntriesFormat(specificErrorResult, 3, 'error', jobName); 414 | } 415 | } 416 | }); 417 | 418 | test('should handle parameter combinations reliably across discovered jobs', async () => { 419 | if (discoveredJobNames.length === 0) { 420 | console.warn('No job names discovered, using fallback for parameter testing'); 421 | discoveredJobNames = ['ProcessOrders']; // Use fallback for this test 422 | } 423 | 424 | const testJobName = discoveredJobNames[0]; 425 | const paramCombinations = [ 426 | { limit: 1 }, 427 | { limit: 3, level: 'info' }, 428 | { limit: 2, jobName: testJobName }, 429 | { limit: 2, level: 'error', jobName: testJobName } 430 | ]; 431 | 432 | for (const params of paramCombinations) { 433 | const result = await client.callTool('get_job_log_entries', params); 434 | 435 | assertValidMCPResponse(result); 436 | 437 | if (!result.isError) { 438 | assertSuccessResponse(result); 439 | const text = parseResponseText(result.content[0].text); 440 | 441 | // Should contain expected limit in header 442 | assert.ok(text.includes(`Latest ${params.limit}`), 443 | `Should contain limit ${params.limit} in response`); 444 | 445 | // Should contain expected level if specified 446 | if (params.level) { 447 | assert.ok(text.includes(`${params.level} messages`), 448 | `Should contain level ${params.level} in response`); 449 | } 450 | 451 | // Should contain job name if specified 452 | if (params.jobName) { 453 | assert.ok(text.includes(`from job: ${params.jobName}`), 454 | `Should contain job name ${params.jobName} in response`); 455 | } 456 | } 457 | } 458 | }); 459 | }); 460 | 461 | // Reliability and consistency testing 462 | describe('Functional Reliability', () => { 463 | test('should maintain consistent response structure across multiple calls', async () => { 464 | const calls = []; 465 | 466 | // Make multiple sequential calls to test consistency 467 | for (let i = 0; i < 3; i++) { // Reduced from 5 to 3 for efficiency 468 | const result = await client.callTool('get_job_log_entries', { limit: 2 }); 469 | calls.push(result); 470 | } 471 | 472 | // All calls should have consistent structure 473 | for (const result of calls) { 474 | assertValidMCPResponse(result); 475 | assert.equal(result.content[0].type, 'text'); 476 | assert.equal(typeof result.isError, 'boolean'); 477 | } 478 | 479 | // All successful calls should have similar content structure 480 | const successfulCalls = calls.filter(call => !call.isError); 481 | for (const result of successfulCalls) { 482 | const text = parseResponseText(result.content[0].text); 483 | assert.ok(text.includes('Latest 2 all levels messages'), 484 | 'Should contain consistent header format'); 485 | } 486 | }); 487 | }); 488 | }); 489 | ``` -------------------------------------------------------------------------------- /src/clients/logs/log-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Main log client - lightweight orchestrator that composes specialized modules 3 | */ 4 | 5 | import { Logger } from '../../utils/logger.js'; 6 | import { getCurrentDate, normalizeFilePath } from '../../utils/utils.js'; 7 | import { WebDAVClientManager } from './webdav-client-manager.js'; 8 | import { LogFileReader } from './log-file-reader.js'; 9 | import { LogFileDiscovery } from './log-file-discovery.js'; 10 | import { LogProcessor } from './log-processor.js'; 11 | import { LogAnalyzer } from './log-analyzer.js'; 12 | import { LogFormatter } from './log-formatter.js'; 13 | import { LOG_CONSTANTS, LOG_MESSAGES, JOB_LOG_CONSTANTS } from './log-constants.js'; 14 | import type { 15 | LogLevel, 16 | LogSearchOptions, 17 | WebDAVClientConfig, 18 | } from './log-types.js'; 19 | 20 | // Support for backward compatibility with SFCCConfig 21 | interface SFCCConfig { 22 | hostname?: string; 23 | username?: string; 24 | password?: string; 25 | clientId?: string; 26 | clientSecret?: string; 27 | } 28 | 29 | export class SFCCLogClient { 30 | private logger: Logger; 31 | private webdavManager: WebDAVClientManager; 32 | private fileReader: LogFileReader; 33 | private fileDiscovery: LogFileDiscovery; 34 | private processor: LogProcessor; 35 | private analyzer: LogAnalyzer; 36 | 37 | constructor(config: SFCCConfig | WebDAVClientConfig, logger?: Logger) { 38 | this.logger = logger ?? Logger.getChildLogger('LogClient'); 39 | this.webdavManager = new WebDAVClientManager(this.logger); 40 | 41 | // Convert SFCCConfig to WebDAVClientConfig for backward compatibility 42 | const webdavConfig: WebDAVClientConfig = { 43 | hostname: config.hostname!, 44 | username: config.username, 45 | password: config.password, 46 | clientId: config.clientId, 47 | clientSecret: config.clientSecret, 48 | }; 49 | 50 | // Setup WebDAV client and initialize modules 51 | const webdavClient = this.webdavManager.setupClient(webdavConfig); 52 | this.fileReader = new LogFileReader(webdavClient, this.logger); 53 | this.fileDiscovery = new LogFileDiscovery(webdavClient, this.logger); 54 | this.processor = new LogProcessor(this.logger); 55 | this.analyzer = new LogAnalyzer(this.logger); 56 | } 57 | 58 | /** 59 | * Get the latest log entries for a specific log level 60 | */ 61 | async getLatestLogs(level: LogLevel, limit: number, date?: string): Promise<string> { 62 | const targetDate = date ?? getCurrentDate(); 63 | this.logger.methodEntry('getLatestLogs', { level, limit, date: targetDate }); 64 | 65 | const startTime = Date.now(); 66 | 67 | // Get and filter log files 68 | const levelFiles = await this.fileDiscovery.getLogFilesByLevel(level, targetDate); 69 | 70 | if (levelFiles.length === 0) { 71 | const allFiles = await this.fileDiscovery.getLogFiles(targetDate); 72 | const availableFiles = allFiles.map(f => normalizeFilePath(f.filename)); 73 | const result = LogFormatter.formatNoFilesFound(level, targetDate, availableFiles); 74 | this.logger.warn(result); 75 | this.logger.methodExit('getLatestLogs', { result: 'no_files' }); 76 | return result; 77 | } 78 | 79 | // Sort files by date (newest first) 80 | const sortedFiles = this.fileDiscovery.sortFilesByDate(levelFiles, true); 81 | 82 | // Read file contents 83 | const fileContents = await this.fileReader.readMultipleFiles( 84 | sortedFiles.map(f => f.filename), 85 | { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }, 86 | ); 87 | 88 | // Process log entries 89 | const allLogEntries = await this.processor.processLogFiles(sortedFiles, level, fileContents); 90 | const sortedEntries = this.processor.sortAndLimitEntries(allLogEntries, limit); 91 | const latestEntries = this.processor.extractFormattedEntries(sortedEntries); 92 | 93 | // Format response 94 | const fileList = sortedFiles.map(f => normalizeFilePath(f.filename)); 95 | const result = LogFormatter.formatLatestLogs(latestEntries, level, limit, fileList); 96 | 97 | this.logger.debug(LogFormatter.formatProcessingSummary( 98 | latestEntries.length, 99 | sortedFiles.length, 100 | allLogEntries.length, 101 | )); 102 | this.logger.timing('getLatestLogs', startTime); 103 | this.logger.methodExit('getLatestLogs', { 104 | entriesReturned: latestEntries.length, 105 | filesProcessed: sortedFiles.length, 106 | }); 107 | 108 | return result; 109 | } 110 | 111 | /** 112 | * Get list of log files for a specific date (backward compatibility) 113 | */ 114 | async getLogFiles(date?: string): Promise<Array<{ filename: string; lastmod: string }>> { 115 | const targetDate = date ?? getCurrentDate(); 116 | this.logger.methodEntry('getLogFiles', { date: targetDate }); 117 | 118 | const logFiles = await this.fileDiscovery.getLogFiles(targetDate); 119 | 120 | this.logger.methodExit('getLogFiles', { count: logFiles.length }); 121 | return logFiles; 122 | } 123 | 124 | /** 125 | * Generate a comprehensive summary of logs for a specific date 126 | */ 127 | async summarizeLogs(date?: string): Promise<string> { 128 | const targetDate = date ?? getCurrentDate(); 129 | this.logger.methodEntry('summarizeLogs', { date: targetDate }); 130 | 131 | const logFiles = await this.fileDiscovery.getLogFiles(targetDate); 132 | 133 | if (logFiles.length === 0) { 134 | const result = `No log files found for date ${targetDate}`; 135 | this.logger.methodExit('summarizeLogs', { result: 'no_files' }); 136 | return result; 137 | } 138 | 139 | // Read file contents 140 | const fileContents = await this.fileReader.readMultipleFiles( 141 | logFiles.map(f => f.filename), 142 | { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }, 143 | ); 144 | 145 | // Analyze logs 146 | const summary = await this.analyzer.analyzeLogs(logFiles, fileContents, targetDate); 147 | const result = LogFormatter.formatLogSummary(summary); 148 | 149 | this.logger.methodExit('summarizeLogs', { filesAnalyzed: logFiles.length }); 150 | return result; 151 | } 152 | 153 | /** 154 | * Search for specific patterns across log files 155 | */ 156 | async searchLogs(options: LogSearchOptions): Promise<string>; 157 | async searchLogs(pattern: string, logLevel?: LogLevel, limit?: number, date?: string): Promise<string>; 158 | async searchLogs( 159 | optionsOrPattern: LogSearchOptions | string, 160 | logLevel?: LogLevel, 161 | limit: number = LOG_CONSTANTS.DEFAULT_SEARCH_LIMIT, 162 | date?: string, 163 | ): Promise<string> { 164 | // Handle both new options interface and legacy parameters 165 | const options: LogSearchOptions = typeof optionsOrPattern === 'string' 166 | ? { 167 | pattern: optionsOrPattern, 168 | logLevel, 169 | limit, 170 | date, 171 | } 172 | : optionsOrPattern; 173 | 174 | const { pattern, logLevel: level, limit: searchLimit, date: searchDate } = options; 175 | const targetDate = searchDate ?? getCurrentDate(); 176 | this.logger.methodEntry('searchLogs', { pattern, logLevel: level, limit: searchLimit, date: targetDate }); 177 | 178 | const logFiles = await this.fileDiscovery.getLogFiles(targetDate); 179 | 180 | // Filter by log level if specified 181 | const filesToSearch = level 182 | ? this.fileDiscovery.filterLogFiles(logFiles, { level }) 183 | : logFiles; 184 | 185 | if (filesToSearch.length === 0) { 186 | const result = LOG_MESSAGES.NO_SEARCH_MATCHES(pattern, targetDate); 187 | this.logger.methodExit('searchLogs', { result: 'no_files' }); 188 | return result; 189 | } 190 | 191 | // Read file contents 192 | const fileContents = await this.fileReader.readMultipleFiles( 193 | filesToSearch.map(f => f.filename), 194 | { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }, 195 | ); 196 | 197 | // Search for patterns 198 | const matches = this.processor.processSearchResults(filesToSearch, fileContents, pattern, searchLimit); 199 | const result = LogFormatter.formatSearchResults(matches, pattern, targetDate); 200 | 201 | this.logger.methodExit('searchLogs', { matchesFound: matches.length }); 202 | return result; 203 | } 204 | 205 | /** 206 | * List available log files with metadata 207 | */ 208 | async listLogFiles(): Promise<string> { 209 | this.logger.methodEntry('listLogFiles'); 210 | 211 | const startTime = Date.now(); 212 | try { 213 | const files = await this.fileDiscovery.getAllLogFiles(); 214 | const result = LogFormatter.formatLogFilesList(files); 215 | this.logger.methodExit('listLogFiles', { fileCount: files.length }); 216 | return result; 217 | } catch (error) { 218 | const errorMessage = LogFormatter.formatError('list_log_files', error); 219 | this.logger.error(errorMessage); 220 | this.logger.methodExit('listLogFiles', { error: true }); 221 | throw new Error(`Failed to list log files: ${(error as Error).message}`); 222 | } finally { 223 | const duration = Date.now() - startTime; 224 | this.logger.debug(`listLogFiles completed in ${duration}ms`); 225 | } 226 | } 227 | 228 | /** 229 | * Get the complete contents of a specific log file 230 | */ 231 | async getLogFileContents(filename: string, maxBytes?: number, tailOnly?: boolean): Promise<string> { 232 | this.logger.methodEntry('getLogFileContents', { filename, maxBytes, tailOnly }); 233 | 234 | const startTime = Date.now(); 235 | try { 236 | // Use tailOnly flag to determine reading strategy 237 | if (tailOnly) { 238 | const content = await this.fileReader.getFileContentsTail(filename, { 239 | maxBytes: maxBytes ?? LOG_CONSTANTS.DEFAULT_TAIL_BYTES, 240 | }); 241 | const result = this.formatLogFileContents(filename, content, true); 242 | this.logger.methodExit('getLogFileContents', { tailOnly: true }); 243 | return result; 244 | } else { 245 | // Read full file from beginning with optional size limit 246 | const content = await this.fileReader.getFileContentsHead(filename, maxBytes); 247 | const result = this.formatLogFileContents(filename, content, false); 248 | this.logger.methodExit('getLogFileContents', { tailOnly: false }); 249 | return result; 250 | } 251 | } catch (error) { 252 | const errorMessage = LogFormatter.formatError('get_log_file_contents', error); 253 | this.logger.error(errorMessage); 254 | this.logger.methodExit('getLogFileContents', { error: true }); 255 | return errorMessage; 256 | } finally { 257 | const duration = Date.now() - startTime; 258 | this.logger.debug(`getLogFileContents completed in ${duration}ms`); 259 | } 260 | } 261 | 262 | /** 263 | * Format log file contents for display 264 | */ 265 | private formatLogFileContents(filename: string, content: string, isTailOnly: boolean): string { 266 | const lines = content.split('\n').filter(line => line.trim()); 267 | const readType = isTailOnly ? 'tail' : 'full'; 268 | 269 | return `# Log File Contents: ${filename} (${readType} read) 270 | 271 | Total lines: ${lines.length} 272 | Content size: ${content.length} bytes 273 | 274 | --- 275 | 276 | ${content}`; 277 | } 278 | 279 | /** 280 | * Get advanced log analysis with patterns and recommendations 281 | */ 282 | async getAdvancedAnalysis(date?: string): Promise<string> { 283 | const targetDate = date ?? getCurrentDate(); 284 | this.logger.methodEntry('getAdvancedAnalysis', { date: targetDate }); 285 | 286 | const logFiles = await this.fileDiscovery.getLogFiles(targetDate); 287 | 288 | if (logFiles.length === 0) { 289 | return `No log files found for date ${targetDate}`; 290 | } 291 | 292 | // Read file contents 293 | const fileContents = await this.fileReader.readMultipleFiles( 294 | logFiles.map(f => f.filename), 295 | { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }, 296 | ); 297 | 298 | // Perform comprehensive analysis 299 | const summary = await this.analyzer.analyzeLogs(logFiles, fileContents, targetDate); 300 | 301 | // Parse entries for pattern detection 302 | const allEntries = Array.from(fileContents.values()) 303 | .flatMap(content => content.split('\n')) 304 | .filter(line => line.trim()) 305 | .map(line => this.processor.parseLogEntry(line)); 306 | 307 | const patterns = this.analyzer.detectPatterns(allEntries); 308 | const healthScore = this.analyzer.calculateHealthScore(summary); 309 | const recommendations = this.analyzer.generateRecommendations(summary, patterns); 310 | 311 | const result = this.analyzer.formatAnalysisResults(summary, patterns, healthScore, recommendations); 312 | 313 | this.logger.methodExit('getAdvancedAnalysis', { 314 | filesAnalyzed: logFiles.length, 315 | entriesProcessed: allEntries.length, 316 | }); 317 | 318 | return result; 319 | } 320 | 321 | /** 322 | * Test WebDAV connection 323 | */ 324 | async testConnection(): Promise<boolean> { 325 | return await this.webdavManager.testConnection(); 326 | } 327 | 328 | /** 329 | * Get log statistics for a date range 330 | */ 331 | async getLogStats(date?: string): Promise<string> { 332 | const targetDate = date ?? getCurrentDate(); 333 | const stats = await this.fileDiscovery.getLogFileStats(targetDate); 334 | 335 | const sections = [ 336 | `Log Statistics for ${targetDate}:`, 337 | '', 338 | '📊 Overview:', 339 | `- Total Files: ${stats.totalFiles}`, 340 | `- Files by Level: ${LogFormatter.formatLogLevelStats(stats.filesByLevel)}`, 341 | '', 342 | '📁 File Info:', 343 | `- Newest: ${stats.newestFile ?? 'N/A'}`, 344 | `- Oldest: ${stats.oldestFile ?? 'N/A'}`, 345 | ]; 346 | 347 | return sections.join('\n'); 348 | } 349 | 350 | /** 351 | * Get latest job log files 352 | */ 353 | async getLatestJobLogFiles(limit?: number): Promise<string> { 354 | this.logger.methodEntry('getLatestJobLogFiles', { limit }); 355 | 356 | try { 357 | const jobLogs = await this.fileDiscovery.getLatestJobLogFiles(limit); 358 | const result = LogFormatter.formatJobLogList(jobLogs); 359 | this.logger.methodExit('getLatestJobLogFiles', { count: jobLogs.length }); 360 | return result; 361 | } catch (error) { 362 | const errorMessage = LogFormatter.formatError('get_latest_job_log_files', error); 363 | this.logger.error(errorMessage); 364 | this.logger.methodExit('getLatestJobLogFiles', { error: true }); 365 | return errorMessage; 366 | } 367 | } 368 | 369 | /** 370 | * Search job logs by job name 371 | */ 372 | async searchJobLogsByName(jobName: string, limit?: number): Promise<string> { 373 | this.logger.methodEntry('searchJobLogsByName', { jobName, limit }); 374 | 375 | try { 376 | const jobLogs = await this.fileDiscovery.searchJobLogsByName(jobName, limit); 377 | const result = LogFormatter.formatJobLogList(jobLogs); 378 | this.logger.methodExit('searchJobLogsByName', { count: jobLogs.length }); 379 | return result; 380 | } catch (error) { 381 | const errorMessage = LogFormatter.formatError('search_job_logs_by_name', error); 382 | this.logger.error(errorMessage); 383 | this.logger.methodExit('searchJobLogsByName', { error: true }); 384 | return errorMessage; 385 | } 386 | } 387 | 388 | /** 389 | * Get job log entries for a specific log level or all levels 390 | */ 391 | async getJobLogEntries( 392 | level: LogLevel | 'all' = 'all', 393 | limit: number = JOB_LOG_CONSTANTS.DEFAULT_JOB_LOG_LIMIT, 394 | jobName?: string, 395 | ): Promise<string> { 396 | this.logger.methodEntry('getJobLogEntries', { level, limit, jobName }); 397 | 398 | try { 399 | // Get job logs based on filter 400 | const jobLogs = jobName 401 | ? await this.fileDiscovery.searchJobLogsByName(jobName, limit) 402 | : await this.fileDiscovery.getLatestJobLogFiles(limit); 403 | 404 | if (jobLogs.length === 0) { 405 | const result = jobName 406 | ? `No job logs found for job name: ${jobName}` 407 | : 'No job logs found'; 408 | this.logger.methodExit('getJobLogEntries', { result: 'no_logs' }); 409 | return result; 410 | } 411 | 412 | // Read job log contents 413 | const fileContents = await this.fileReader.readMultipleFiles( 414 | jobLogs.map(job => job.logFile), 415 | { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }, 416 | ); 417 | 418 | // Process job log entries 419 | const jobLogEntries = await this.processor.processJobLogFiles(jobLogs, level, fileContents); 420 | const sortedEntries = this.processor.sortAndLimitEntries(jobLogEntries, limit); 421 | const latestEntries = this.processor.extractFormattedEntries(sortedEntries); 422 | 423 | // Format response 424 | const jobContext = jobName ? `job: ${jobName}` : 'latest jobs'; 425 | const result = LogFormatter.formatJobLogEntries(latestEntries, level, limit, jobContext); 426 | 427 | this.logger.methodExit('getJobLogEntries', { 428 | entriesReturned: latestEntries.length, 429 | jobLogsProcessed: jobLogs.length, 430 | }); 431 | 432 | return result; 433 | } catch (error) { 434 | const errorMessage = LogFormatter.formatError('get_job_log_entries', error); 435 | this.logger.error(errorMessage); 436 | this.logger.methodExit('getJobLogEntries', { error: true }); 437 | return errorMessage; 438 | } 439 | } 440 | 441 | /** 442 | * Search for patterns in job logs 443 | */ 444 | async searchJobLogs( 445 | pattern: string, 446 | level?: LogLevel | 'all', 447 | limit: number = LOG_CONSTANTS.DEFAULT_SEARCH_LIMIT, 448 | jobName?: string, 449 | ): Promise<string> { 450 | this.logger.methodEntry('searchJobLogs', { pattern, level, limit, jobName }); 451 | 452 | try { 453 | // Get job logs based on filter 454 | const jobLogs = jobName 455 | ? await this.fileDiscovery.searchJobLogsByName(jobName) 456 | : await this.fileDiscovery.getLatestJobLogFiles(); 457 | 458 | if (jobLogs.length === 0) { 459 | const result = jobName 460 | ? `No job logs found for job name: ${jobName}` 461 | : 'No job logs found'; 462 | this.logger.methodExit('searchJobLogs', { result: 'no_logs' }); 463 | return result; 464 | } 465 | 466 | // Read job log contents 467 | const fileContents = await this.fileReader.readMultipleFiles( 468 | jobLogs.map(job => job.logFile), 469 | { maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES }, 470 | ); 471 | 472 | // Search for patterns in job logs 473 | const matches: string[] = []; 474 | for (const jobLog of jobLogs) { 475 | const content = fileContents.get(jobLog.logFile); 476 | if (!content) { 477 | continue; 478 | } 479 | 480 | const lines = content.split('\n'); 481 | for (const line of lines) { 482 | if (line.toLowerCase().includes(pattern.toLowerCase()) && matches.length < limit) { 483 | // Filter by level if specified 484 | if (level && level !== 'all') { 485 | const levelUpper = level.toUpperCase(); 486 | if (!line.includes(` ${levelUpper} `)) { 487 | continue; 488 | } 489 | } 490 | matches.push(`[${jobLog.jobName}] ${line.trim()}`); 491 | } 492 | } 493 | } 494 | 495 | const jobContext = jobName ? `job: ${jobName}` : 'job logs'; 496 | const result = LogFormatter.formatJobSearchResults(matches, pattern, jobContext); 497 | 498 | this.logger.methodExit('searchJobLogs', { matchesFound: matches.length }); 499 | return result; 500 | } catch (error) { 501 | const errorMessage = LogFormatter.formatError('search_job_logs', error); 502 | this.logger.error(errorMessage); 503 | this.logger.methodExit('searchJobLogs', { error: true }); 504 | return errorMessage; 505 | } 506 | } 507 | 508 | /** 509 | * Get job execution summary for a specific job 510 | */ 511 | async getJobExecutionSummary(jobName: string): Promise<string> { 512 | this.logger.methodEntry('getJobExecutionSummary', { jobName }); 513 | 514 | try { 515 | const jobLogs = await this.fileDiscovery.searchJobLogsByName(jobName, 1); 516 | 517 | if (jobLogs.length === 0) { 518 | const result = `No job logs found for job name: ${jobName}`; 519 | this.logger.methodExit('getJobExecutionSummary', { result: 'no_logs' }); 520 | return result; 521 | } 522 | 523 | const latestJobLog = jobLogs[0]; 524 | const content = await this.fileReader.getFileContentsTail(latestJobLog.logFile, { 525 | maxBytes: LOG_CONSTANTS.DEFAULT_TAIL_BYTES, 526 | }); 527 | 528 | const summary = this.processor.extractJobExecutionSummary(content); 529 | const result = LogFormatter.formatJobExecutionSummary(summary, jobName); 530 | 531 | this.logger.methodExit('getJobExecutionSummary', { jobLog: latestJobLog.logFile }); 532 | return result; 533 | } catch (error) { 534 | const errorMessage = LogFormatter.formatError('get_job_execution_summary', error); 535 | this.logger.error(errorMessage); 536 | this.logger.methodExit('getJobExecutionSummary', { error: true }); 537 | return errorMessage; 538 | } 539 | } 540 | } 541 | ``` -------------------------------------------------------------------------------- /tests/utils.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | getCurrentDate, 3 | formatBytes, 4 | parseLogEntries, 5 | extractUniqueErrors, 6 | normalizeFilePath, 7 | extractTimestampFromLogEntry, 8 | } from '../src/utils/utils'; 9 | 10 | describe('utils.ts', () => { 11 | describe('getCurrentDate', () => { 12 | it('should return current date in YYYYMMDD format', () => { 13 | const result = getCurrentDate(); 14 | 15 | // Should be 8 characters long 16 | expect(result).toHaveLength(8); 17 | 18 | // Should match YYYYMMDD pattern 19 | expect(result).toMatch(/^\d{8}$/); 20 | 21 | // Should be a valid date when parsed 22 | const year = parseInt(result.substring(0, 4)); 23 | const month = parseInt(result.substring(4, 6)); 24 | const day = parseInt(result.substring(6, 8)); 25 | 26 | expect(year).toBeGreaterThan(2020); 27 | expect(month).toBeGreaterThanOrEqual(1); 28 | expect(month).toBeLessThanOrEqual(12); 29 | expect(day).toBeGreaterThanOrEqual(1); 30 | expect(day).toBeLessThanOrEqual(31); 31 | }); 32 | 33 | it('should return today\'s date', () => { 34 | const now = new Date(); 35 | const expected = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`; 36 | 37 | expect(getCurrentDate()).toBe(expected); 38 | }); 39 | 40 | it('should pad single digit months and days with zeros', () => { 41 | // Mock Date to return January 5th, 2023 42 | const mockDate = new Date('2023-01-05T10:00:00Z'); 43 | jest.spyOn(global, 'Date').mockImplementation(() => mockDate); 44 | 45 | const result = getCurrentDate(); 46 | expect(result).toBe('20230105'); 47 | 48 | jest.restoreAllMocks(); 49 | }); 50 | }); 51 | 52 | describe('formatBytes', () => { 53 | it('should format zero bytes', () => { 54 | expect(formatBytes(0)).toBe('0 Bytes'); 55 | }); 56 | 57 | it('should format bytes (less than 1024)', () => { 58 | expect(formatBytes(512)).toBe('512 Bytes'); 59 | expect(formatBytes(1023)).toBe('1023 Bytes'); 60 | expect(formatBytes(1)).toBe('1 Bytes'); 61 | }); 62 | 63 | it('should format kilobytes', () => { 64 | expect(formatBytes(1024)).toBe('1 KB'); 65 | expect(formatBytes(1536)).toBe('1.5 KB'); // 1024 + 512 66 | expect(formatBytes(2048)).toBe('2 KB'); 67 | expect(formatBytes(1024 * 1023)).toBe('1023 KB'); 68 | }); 69 | 70 | it('should format megabytes', () => { 71 | expect(formatBytes(1024 * 1024)).toBe('1 MB'); 72 | expect(formatBytes(1024 * 1024 * 1.5)).toBe('1.5 MB'); 73 | expect(formatBytes(1024 * 1024 * 2.75)).toBe('2.75 MB'); 74 | expect(formatBytes(1024 * 1024 * 1023)).toBe('1023 MB'); 75 | }); 76 | 77 | it('should format gigabytes', () => { 78 | expect(formatBytes(1024 * 1024 * 1024)).toBe('1 GB'); 79 | expect(formatBytes(1024 * 1024 * 1024 * 2.5)).toBe('2.5 GB'); 80 | expect(formatBytes(1024 * 1024 * 1024 * 10.25)).toBe('10.25 GB'); 81 | }); 82 | 83 | it('should handle decimal values correctly', () => { 84 | expect(formatBytes(1536.7)).toBe('1.5 KB'); 85 | expect(formatBytes(2097152.5)).toBe('2 MB'); 86 | }); 87 | 88 | it('should round to 2 decimal places', () => { 89 | expect(formatBytes(1126.4)).toBe('1.1 KB'); // 1126.4 / 1024 = 1.1000390625 90 | expect(formatBytes(1234567)).toBe('1.18 MB'); // Should round to 2 decimal places 91 | }); 92 | 93 | it('should handle large numbers', () => { 94 | const largeNumber = 1024 * 1024 * 1024 * 1000; // 1TB in bytes 95 | expect(formatBytes(largeNumber)).toBe('1000 GB'); 96 | }); 97 | }); 98 | 99 | describe('parseLogEntries', () => { 100 | it('should parse single log entry', () => { 101 | const content = '[2023-08-09T10:30:00.123 GMT] ERROR SomeClass - This is an error message'; 102 | const result = parseLogEntries(content, 'ERROR'); 103 | 104 | expect(result).toHaveLength(1); 105 | expect(result[0]).toBe('[2023-08-09T10:30:00.123 GMT] ERROR SomeClass - This is an error message'); 106 | }); 107 | 108 | it('should parse multiple log entries of same level', () => { 109 | const content = `[2023-08-09T10:30:00.123 GMT] ERROR Class1 - First error 110 | [2023-08-09T10:31:00.456 GMT] ERROR Class2 - Second error 111 | [2023-08-09T10:32:00.789 GMT] ERROR Class3 - Third error`; 112 | 113 | const result = parseLogEntries(content, 'ERROR'); 114 | 115 | expect(result).toHaveLength(3); 116 | expect(result[0]).toBe('[2023-08-09T10:30:00.123 GMT] ERROR Class1 - First error'); 117 | expect(result[1]).toBe('[2023-08-09T10:31:00.456 GMT] ERROR Class2 - Second error'); 118 | expect(result[2]).toBe('[2023-08-09T10:32:00.789 GMT] ERROR Class3 - Third error'); 119 | }); 120 | 121 | it('should filter by log level', () => { 122 | const content = `[2023-08-09T10:30:00.123 GMT] ERROR Class1 - Error message 123 | [2023-08-09T10:31:00.456 GMT] WARN Class2 - Warning message 124 | [2023-08-09T10:32:00.789 GMT] INFO Class3 - Info message 125 | [2023-08-09T10:33:00.012 GMT] ERROR Class4 - Another error`; 126 | 127 | const errorResult = parseLogEntries(content, 'ERROR'); 128 | const warnResult = parseLogEntries(content, 'WARN'); 129 | const infoResult = parseLogEntries(content, 'INFO'); 130 | 131 | expect(errorResult).toHaveLength(2); 132 | expect(warnResult).toHaveLength(1); 133 | expect(infoResult).toHaveLength(1); 134 | 135 | expect(errorResult[0]).toContain('Error message'); 136 | expect(errorResult[1]).toContain('Another error'); 137 | expect(warnResult[0]).toContain('Warning message'); 138 | expect(infoResult[0]).toContain('Info message'); 139 | }); 140 | 141 | it('should handle multi-line log entries', () => { 142 | const content = `[2023-08-09T10:30:00.123 GMT] ERROR Class1 - Error with stack trace 143 | at function1 (file1.js:10:5) 144 | at function2 (file2.js:20:3) 145 | at main (app.js:100:1) 146 | [2023-08-09T10:31:00.456 GMT] ERROR Class2 - Another error`; 147 | 148 | const result = parseLogEntries(content, 'ERROR'); 149 | 150 | expect(result).toHaveLength(2); 151 | expect(result[0]).toContain('Error with stack trace'); 152 | expect(result[0]).toContain('at function1 (file1.js:10:5)'); 153 | expect(result[0]).toContain('at function2 (file2.js:20:3)'); 154 | expect(result[0]).toContain('at main (app.js:100:1)'); 155 | expect(result[1]).toBe('[2023-08-09T10:31:00.456 GMT] ERROR Class2 - Another error'); 156 | }); 157 | 158 | it('should handle empty content', () => { 159 | expect(parseLogEntries('', 'ERROR')).toEqual([]); 160 | expect(parseLogEntries(' ', 'ERROR')).toEqual([]); 161 | }); 162 | 163 | it('should handle content with no matching log level', () => { 164 | const content = `[2023-08-09T10:30:00.123 GMT] WARN Class1 - Warning message 165 | [2023-08-09T10:31:00.456 GMT] INFO Class2 - Info message`; 166 | 167 | expect(parseLogEntries(content, 'ERROR')).toEqual([]); 168 | }); 169 | 170 | it('should ignore lines that don\'t match the log pattern', () => { 171 | const content = `Some random text 172 | [2023-08-09T10:30:00.123 GMT] ERROR Class1 - Valid error 173 | [2023-08-09T10:31:00.456 GMT] ERROR Class2 - Another valid error`; 174 | 175 | const result = parseLogEntries(content, 'ERROR'); 176 | 177 | expect(result).toHaveLength(2); 178 | expect(result[0]).toBe('[2023-08-09T10:30:00.123 GMT] ERROR Class1 - Valid error'); 179 | expect(result[1]).toBe('[2023-08-09T10:31:00.456 GMT] ERROR Class2 - Another valid error'); 180 | }); 181 | 182 | it('should handle edge case with GMT requirement', () => { 183 | const content = `[2023-08-09T10:30:00.123] ERROR Class1 - Error without GMT 184 | [2023-08-09T10:31:00.456 GMT] ERROR Class2 - Error with GMT`; 185 | 186 | const result = parseLogEntries(content, 'ERROR'); 187 | 188 | expect(result).toHaveLength(1); 189 | expect(result[0]).toBe('[2023-08-09T10:31:00.456 GMT] ERROR Class2 - Error with GMT'); 190 | }); 191 | 192 | it('should trim entries properly', () => { 193 | const content = ` [2023-08-09T10:30:00.123 GMT] ERROR Class1 - Error with leading spaces 194 | [2023-08-09T10:31:00.456 GMT] ERROR Class2 - Another error `; 195 | 196 | const result = parseLogEntries(content, 'ERROR'); 197 | 198 | expect(result).toHaveLength(2); 199 | expect(result[0]).toBe('[2023-08-09T10:30:00.123 GMT] ERROR Class1 - Error with leading spaces'); 200 | expect(result[1]).toBe('[2023-08-09T10:31:00.456 GMT] ERROR Class2 - Another error'); 201 | }); 202 | }); 203 | 204 | describe('extractUniqueErrors', () => { 205 | it('should extract unique error patterns', () => { 206 | const errors = [ 207 | '[2023-08-09T10:30:00.123 GMT] ERROR ClassName1 - Database connection failed', 208 | '[2023-08-09T10:31:00.456 GMT] ERROR ClassName2 - File not found', 209 | '[2023-08-09T10:32:00.789 GMT] ERROR ClassName1 - Database connection failed', 210 | '[2023-08-09T10:33:00.012 GMT] ERROR ClassName3 - Permission denied', 211 | ]; 212 | 213 | const result = extractUniqueErrors(errors); 214 | 215 | expect(result).toHaveLength(3); 216 | expect(result).toContain('Database connection failed'); 217 | expect(result).toContain('File not found'); 218 | expect(result).toContain('Permission denied'); 219 | }); 220 | 221 | it('should limit results to top 10 unique errors', () => { 222 | const errors = []; 223 | for (let i = 1; i <= 15; i++) { 224 | errors.push(`[2023-08-09T10:30:00.123 GMT] ERROR Class${i} - Error message ${i}`); 225 | } 226 | 227 | const result = extractUniqueErrors(errors); 228 | 229 | expect(result).toHaveLength(10); 230 | }); 231 | 232 | it('should handle empty array', () => { 233 | expect(extractUniqueErrors([])).toEqual([]); 234 | }); 235 | 236 | it('should handle errors without proper format', () => { 237 | const errors = [ 238 | 'Invalid log format', 239 | '[2023-08-09T10:30:00.123 GMT] ERROR ClassName - Valid error message', 240 | 'Another invalid format', 241 | ]; 242 | 243 | const result = extractUniqueErrors(errors); 244 | 245 | expect(result).toHaveLength(1); 246 | expect(result[0]).toBe('Valid error message'); 247 | }); 248 | 249 | it('should extract error message from multi-line entries', () => { 250 | const errors = [ 251 | `[2023-08-09T10:30:00.123 GMT] ERROR ClassName - Connection timeout 252 | at database.connect() 253 | at service.initialize()`, 254 | '[2023-08-09T10:31:00.456 GMT] ERROR AnotherClass - File access denied', 255 | ]; 256 | 257 | const result = extractUniqueErrors(errors); 258 | 259 | expect(result).toHaveLength(2); 260 | expect(result).toContain('Connection timeout'); 261 | expect(result).toContain('File access denied'); 262 | }); 263 | 264 | it('should handle different class name formats', () => { 265 | const errors = [ 266 | '[2023-08-09T10:30:00.123 GMT] ERROR dw.system.Pipeline - Pipeline execution failed', 267 | '[2023-08-09T10:31:00.456 GMT] ERROR CustomClass123 - Custom error occurred', 268 | '[2023-08-09T10:32:00.789 GMT] ERROR com.demandware.Core - Core system error', 269 | ]; 270 | 271 | const result = extractUniqueErrors(errors); 272 | 273 | expect(result).toHaveLength(3); 274 | expect(result).toContain('Pipeline execution failed'); 275 | expect(result).toContain('Custom error occurred'); 276 | expect(result).toContain('Core system error'); 277 | }); 278 | 279 | it('should trim extracted error messages', () => { 280 | const errors = [ 281 | '[2023-08-09T10:30:00.123 GMT] ERROR ClassName - Error with extra spaces ', 282 | ]; 283 | 284 | const result = extractUniqueErrors(errors); 285 | 286 | expect(result).toHaveLength(1); 287 | expect(result[0]).toBe('Error with extra spaces'); 288 | }); 289 | 290 | it('should maintain order of first occurrence', () => { 291 | const errors = [ 292 | '[2023-08-09T10:30:00.123 GMT] ERROR Class1 - Third error alphabetically', 293 | '[2023-08-09T10:31:00.456 GMT] ERROR Class2 - First error alphabetically', 294 | '[2023-08-09T10:32:00.789 GMT] ERROR Class3 - Second error alphabetically', 295 | ]; 296 | 297 | const result = extractUniqueErrors(errors); 298 | 299 | expect(result).toHaveLength(3); 300 | expect(result[0]).toBe('Third error alphabetically'); 301 | expect(result[1]).toBe('First error alphabetically'); 302 | expect(result[2]).toBe('Second error alphabetically'); 303 | }); 304 | }); 305 | 306 | describe('normalizeFilePath', () => { 307 | it('should remove leading slash from file path', () => { 308 | expect(normalizeFilePath('/path/to/file.js')).toBe('path/to/file.js'); 309 | expect(normalizeFilePath('/single')).toBe('single'); 310 | expect(normalizeFilePath('/deep/nested/path/file.txt')).toBe('deep/nested/path/file.txt'); 311 | }); 312 | 313 | it('should leave path unchanged if no leading slash', () => { 314 | expect(normalizeFilePath('path/to/file.js')).toBe('path/to/file.js'); 315 | expect(normalizeFilePath('single')).toBe('single'); 316 | expect(normalizeFilePath('deep/nested/path/file.txt')).toBe('deep/nested/path/file.txt'); 317 | }); 318 | 319 | it('should handle empty string', () => { 320 | expect(normalizeFilePath('')).toBe(''); 321 | }); 322 | 323 | it('should handle single slash', () => { 324 | expect(normalizeFilePath('/')).toBe(''); 325 | }); 326 | 327 | it('should handle multiple leading slashes (only remove first)', () => { 328 | expect(normalizeFilePath('//path/to/file')).toBe('/path/to/file'); 329 | expect(normalizeFilePath('///path/to/file')).toBe('//path/to/file'); 330 | }); 331 | 332 | it('should handle paths with special characters', () => { 333 | expect(normalizeFilePath('/path/with spaces/file.js')).toBe('path/with spaces/file.js'); 334 | expect(normalizeFilePath('/path/with-dashes/file_name.txt')).toBe('path/with-dashes/file_name.txt'); 335 | expect(normalizeFilePath('/path/with.dots/file.name.ext')).toBe('path/with.dots/file.name.ext'); 336 | }); 337 | 338 | it('should handle Windows-style paths', () => { 339 | expect(normalizeFilePath('/C:/Windows/System32/file.dll')).toBe('C:/Windows/System32/file.dll'); 340 | expect(normalizeFilePath('/folder\\subfolder\\file.txt')).toBe('folder\\subfolder\\file.txt'); 341 | }); 342 | 343 | it('should handle paths with query parameters and fragments', () => { 344 | expect(normalizeFilePath('/api/endpoint?param=value')).toBe('api/endpoint?param=value'); 345 | expect(normalizeFilePath('/page.html#section')).toBe('page.html#section'); 346 | expect(normalizeFilePath('/file.js?v=1.0.0&cache=false')).toBe('file.js?v=1.0.0&cache=false'); 347 | }); 348 | }); 349 | 350 | describe('edge cases and integration', () => { 351 | it('should handle all functions with edge case inputs', () => { 352 | // Test all functions with various edge cases 353 | expect(() => getCurrentDate()).not.toThrow(); 354 | expect(() => formatBytes(-1)).not.toThrow(); 355 | expect(() => parseLogEntries('malformed log', 'INVALID')).not.toThrow(); 356 | expect(() => extractUniqueErrors(['malformed'])).not.toThrow(); 357 | expect(() => normalizeFilePath(' /path/with/spaces ')).not.toThrow(); 358 | }); 359 | 360 | it('should handle realistic SFCC log parsing workflow', () => { 361 | const logContent = `[2023-08-09T10:30:00.123 GMT] INFO dw.system.Request - Request started 362 | [2023-08-09T10:30:00.145 GMT] ERROR dw.catalog.ProductMgr - Product not found: ID-12345 363 | at ProductService.getProduct() 364 | at Controller.showPDP() 365 | [2023-08-09T10:30:00.167 GMT] WARN dw.system.Cache - Cache miss for key: product-12345 366 | [2023-08-09T10:30:00.189 GMT] ERROR dw.order.OrderMgr - Order creation failed: insufficient inventory 367 | [2023-08-09T10:30:00.201 GMT] ERROR dw.catalog.ProductMgr - Product not found: ID-67890`; 368 | 369 | // Parse errors 370 | const errors = parseLogEntries(logContent, 'ERROR'); 371 | expect(errors).toHaveLength(3); 372 | 373 | // Extract unique error patterns - there are actually 3 unique errors 374 | const uniqueErrors = extractUniqueErrors(errors); 375 | expect(uniqueErrors).toHaveLength(3); 376 | expect(uniqueErrors).toContain('Product not found: ID-12345'); 377 | expect(uniqueErrors).toContain('Order creation failed: insufficient inventory'); 378 | expect(uniqueErrors).toContain('Product not found: ID-67890'); 379 | 380 | // Parse warnings 381 | const warnings = parseLogEntries(logContent, 'WARN'); 382 | expect(warnings).toHaveLength(1); 383 | 384 | // Parse info 385 | const info = parseLogEntries(logContent, 'INFO'); 386 | expect(info).toHaveLength(1); 387 | }); 388 | 389 | it('should handle performance with large datasets', () => { 390 | // Test with larger datasets to ensure reasonable performance 391 | const largeLogContent = Array(1000).fill(0).map((_, i) => 392 | `[2023-08-09T10:30:${String(i % 60).padStart(2, '0')}.123 GMT] ERROR Class${i % 10} - Error message ${i}`, 393 | ).join('\n'); 394 | 395 | const start = Date.now(); 396 | const errors = parseLogEntries(largeLogContent, 'ERROR'); 397 | const uniqueErrors = extractUniqueErrors(errors); 398 | const duration = Date.now() - start; 399 | 400 | expect(errors).toHaveLength(1000); 401 | expect(uniqueErrors).toHaveLength(10); // Limited to 10 unique errors 402 | expect(duration).toBeLessThan(1000); // Should complete within 1 second 403 | }); 404 | }); 405 | 406 | describe('extractTimestampFromLogEntry', () => { 407 | it('should extract timestamp from valid log entry', () => { 408 | const logEntry = '[2025-08-19T10:30:00.000 GMT] ERROR Class - Test message'; 409 | const result = extractTimestampFromLogEntry(logEntry); 410 | 411 | expect(result).toBeInstanceOf(Date); 412 | expect(result?.getUTCFullYear()).toBe(2025); 413 | expect(result?.getUTCMonth()).toBe(7); // 0-based months (August = 7) 414 | expect(result?.getUTCDate()).toBe(19); 415 | expect(result?.getUTCHours()).toBe(10); 416 | expect(result?.getUTCMinutes()).toBe(30); 417 | }); 418 | 419 | it('should handle different time values', () => { 420 | const logEntry = '[2025-12-31T23:59:59.999 GMT] WARN Class - End of year'; 421 | const result = extractTimestampFromLogEntry(logEntry); 422 | 423 | expect(result).toBeInstanceOf(Date); 424 | expect(result?.getUTCFullYear()).toBe(2025); 425 | expect(result?.getUTCMonth()).toBe(11); // December = 11 426 | expect(result?.getUTCDate()).toBe(31); 427 | expect(result?.getUTCHours()).toBe(23); 428 | expect(result?.getUTCMinutes()).toBe(59); 429 | expect(result?.getUTCSeconds()).toBe(59); 430 | expect(result?.getUTCMilliseconds()).toBe(999); 431 | }); 432 | 433 | it('should return null for entry without timestamp', () => { 434 | const logEntry = 'ERROR Class - No timestamp here'; 435 | const result = extractTimestampFromLogEntry(logEntry); 436 | 437 | expect(result).toBeNull(); 438 | }); 439 | 440 | it('should return null for malformed timestamp', () => { 441 | const logEntry = '[2025-13-45T25:70:70.000 GMT] ERROR Class - Invalid timestamp'; 442 | const result = extractTimestampFromLogEntry(logEntry); 443 | 444 | expect(result).toBeNull(); 445 | }); 446 | 447 | it('should return null for entry without GMT marker', () => { 448 | const logEntry = '[2025-08-19T10:30:00.000] ERROR Class - No GMT marker'; 449 | const result = extractTimestampFromLogEntry(logEntry); 450 | 451 | expect(result).toBeNull(); 452 | }); 453 | 454 | it('should handle entries with continuation lines', () => { 455 | const logEntry = '[2025-08-19T10:30:00.000 GMT] ERROR Class - Stack trace\n at function1()\n at function2()'; 456 | const result = extractTimestampFromLogEntry(logEntry); 457 | 458 | expect(result).toBeInstanceOf(Date); 459 | expect(result?.getUTCHours()).toBe(10); 460 | expect(result?.getUTCMinutes()).toBe(30); 461 | }); 462 | 463 | it('should handle edge case timestamps', () => { 464 | // Test midnight 465 | const midnightEntry = '[2025-01-01T00:00:00.000 GMT] INFO Class - Midnight'; 466 | const midnightResult = extractTimestampFromLogEntry(midnightEntry); 467 | expect(midnightResult?.getUTCHours()).toBe(0); 468 | expect(midnightResult?.getUTCMinutes()).toBe(0); 469 | 470 | // Test leap year 471 | const leapYearEntry = '[2024-02-29T12:00:00.000 GMT] INFO Class - Leap year'; 472 | const leapYearResult = extractTimestampFromLogEntry(leapYearEntry); 473 | expect(leapYearResult?.getMonth()).toBe(1); // February 474 | expect(leapYearResult?.getDate()).toBe(29); 475 | }); 476 | }); 477 | }); 478 | ``` -------------------------------------------------------------------------------- /docs/dw_catalog/ProductAvailabilityModel.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.catalog 2 | 3 | # Class ProductAvailabilityModel 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.catalog.ProductAvailabilityModel 9 | 10 | ## Description 11 | 12 | The ProductAvailabilityModel provides methods for retrieving all information on availability of a single product. When using Omnichannel Inventory (OCI): OCI supports backorders, but does not support preorders or perpetual availability. OCI refers to expected restocks as Future inventory. OCI uses an eventual consistency model with asynchronous inventory data updates. Your code must not assume that inventory-affecting actions, such as placing orders, will immediately change inventory levels. 13 | 14 | ## Constants 15 | 16 | ### AVAILABILITY_STATUS_BACKORDER 17 | 18 | **Type:** String = "BACKORDER" 19 | 20 | Indicates that the product stock has run out, but will be replenished, and is therefore available for ordering. 21 | 22 | ### AVAILABILITY_STATUS_IN_STOCK 23 | 24 | **Type:** String = "IN_STOCK" 25 | 26 | Indicates that the product is in stock and available for ordering. 27 | 28 | ### AVAILABILITY_STATUS_NOT_AVAILABLE 29 | 30 | **Type:** String = "NOT_AVAILABLE" 31 | 32 | Indicates that the product is not currently available for ordering. 33 | 34 | ### AVAILABILITY_STATUS_PREORDER 35 | 36 | **Type:** String = "PREORDER" 37 | 38 | Indicates that the product is not yet in stock but is available for ordering. 39 | 40 | ## Properties 41 | 42 | ### availability 43 | 44 | **Type:** Number (Read Only) 45 | 46 | The availability of the product, which roughly defined is the 47 | ratio of the original stock that is still available to sell. The basic 48 | formula, if the current site uses an 49 | inventory list, is the ATS quantity divided by allocation 50 | amount. If the product is not orderable at all this method returns 0. 51 | The following specific rules apply for standard products: 52 | 53 | If inventory lists are in use: 54 | 55 | If no inventory record exists and the inventory list default-in-stock flag is true this method returns 1. 56 | If no inventory record exists the inventory list default-in-stock flag is false this method returns 0. 57 | If the product is not available this method returns 0. 58 | If the product is perpetually available this method returns 1. 59 | Otherwise, this method returns ATS / (allocation + preorderBackorderAllocation). (Values from ProductInventoryRecord.) 60 | 61 | 62 | 63 | If inventory lists are not in use the method returns 0. 64 | 65 | The following rules apply for special product types: 66 | 67 | For a master product this method returns the average availability 68 | of its online variations. 69 | For a master product with no online variations this method returns 0. 70 | For a master product with own inventory record the rules of the standard 71 | products apply. Note: In this case the availability of the variations is not considered. 72 | For a product set this method returns the greatest availability of 73 | the online products in the set. 74 | For a product set with no online products this method returns 0. 75 | For a product set with an inventory record the rules of the standard 76 | products apply. Note: In this case the availability of the set products is not considered. 77 | For a bundle, this method returns the least availability of the bundled 78 | products according to their bundled quantity and if it exist also from 79 | the bundle inventory record. 80 | 81 | ### availabilityStatus 82 | 83 | **Type:** String (Read Only) 84 | 85 | The availability-status for the minimum-orderable-quantity (MOQ) of 86 | the product. The MOQ essentially represents a single orderable unit, and 87 | therefore can be represented by a single availability-status. This 88 | method is essentially a convenience method. The same information 89 | can be retrieved by calling getAvailabilityLevels(Number) 90 | with the MOQ of the product as the parameter and then retrieving the 91 | single status from the returned map. 92 | 93 | This method is typically used to display a product's availability in 94 | the catalog when the order quantity is not known. 95 | 96 | ### inStock 97 | 98 | **Type:** isInStock(Number) (Read Only) 99 | 100 | Convenience method for isInStock(Number). Returns true, if the 101 | Product is available in the minimum-order-quantity. If the product does 102 | not have a minimum-order-quantity defined, in-stock is checked for a 103 | quantity value 1. 104 | 105 | ### inventoryRecord 106 | 107 | **Type:** ProductInventoryRecord (Read Only) 108 | 109 | The ProductInventoryRecord for the Product associated 110 | with this model. 111 | 112 | ### orderable 113 | 114 | **Type:** isOrderable(Number) (Read Only) 115 | 116 | Convenience method for isOrderable(Number). Returns true if the 117 | Product is currently online (based on its online flag and online dates) 118 | and is orderable in its minimum-order-quantity. If the product does not 119 | have a minimum-order-quantity specified, then 1 is used. The method 120 | returns false otherwise. 121 | 122 | Note: Orderable status is more general than in-stock status. A product 123 | may be out-of-stock but orderable because it is back-orderable or 124 | pre-orderable. 125 | 126 | ### SKUCoverage 127 | 128 | **Type:** Number (Read Only) 129 | 130 | The SKU coverage of the product. The basic formula for a 131 | master product is the ratio of online variations that are in stock 132 | to the total number of online variations. The following specific rules 133 | apply for standard products: 134 | 135 | If the product is in stock this method returns the availability of the product. 136 | If the product is out of stock this method returns 0. 137 | 138 | The following rules apply for special product types: 139 | 140 | For a master product this method returns the average SKU coverage 141 | of its online variations. 142 | For a master product with no online variations this method returns 0. 143 | For a product set this method returns the ratio of orderable SKUs in the product set 144 | over the total number of online SKUs in the product set. 145 | For a product set with no online products this method returns 0. 146 | For a product bundle this method returns 1 if all of the bundled 147 | products are online, and 0 otherwise. 148 | For a product bundle with no online bundled products this method 149 | returns 0. 150 | 151 | ### timeToOutOfStock 152 | 153 | **Type:** Number (Read Only) 154 | 155 | The number of hours before the product is expected to go out 156 | of stock. The basic formula is the ATS quantity divided by the 157 | sales velocity for the most recent day. The following specific rules 158 | apply for standard products: 159 | 160 | If the product is out of stock this method returns 0. 161 | If the product is perpetually available this method returns 1. 162 | If the sales velocity or ATS is not available this method returns 0. 163 | Otherwise this method returns ATS / sales velocity. 164 | 165 | The following rules apply for special product types: 166 | 167 | For a master product this method returns the greatest time to out 168 | of stock of its online variations. 169 | For a master product with no online variations this method returns 0. 170 | For a product set this method returns the greatest time to out 171 | of stock of the online products in the set. 172 | For a product set with no online products this method returns 0. 173 | For a bundle with no product inventory record, this method returns 174 | the least time to out of stock of the online bundled products. 175 | For a bundle with no product inventory record, and no online 176 | bundled products, this method returns 0. 177 | 178 | ## Constructor Summary 179 | 180 | ## Method Summary 181 | 182 | ### getAvailability 183 | 184 | **Signature:** `getAvailability() : Number` 185 | 186 | Returns the availability of the product, which roughly defined is the ratio of the original stock that is still available to sell. 187 | 188 | ### getAvailabilityLevels 189 | 190 | **Signature:** `getAvailabilityLevels(quantity : Number) : ProductAvailabilityLevels` 191 | 192 | Returns an instance of ProductAvailabilityLevels, where each available quantity represents a part of the input quantity. 193 | 194 | ### getAvailabilityStatus 195 | 196 | **Signature:** `getAvailabilityStatus() : String` 197 | 198 | Returns the availability-status for the minimum-orderable-quantity (MOQ) of the product. 199 | 200 | ### getInventoryRecord 201 | 202 | **Signature:** `getInventoryRecord() : ProductInventoryRecord` 203 | 204 | Returns the ProductInventoryRecord for the Product associated with this model. 205 | 206 | ### getSKUCoverage 207 | 208 | **Signature:** `getSKUCoverage() : Number` 209 | 210 | Returns the SKU coverage of the product. 211 | 212 | ### getTimeToOutOfStock 213 | 214 | **Signature:** `getTimeToOutOfStock() : Number` 215 | 216 | Returns the number of hours before the product is expected to go out of stock. 217 | 218 | ### isInStock 219 | 220 | **Signature:** `isInStock(quantity : Number) : boolean` 221 | 222 | Returns true if the Product is in-stock in the given quantity. 223 | 224 | ### isInStock 225 | 226 | **Signature:** `isInStock() : boolean` 227 | 228 | Convenience method for isInStock(Number). 229 | 230 | ### isOrderable 231 | 232 | **Signature:** `isOrderable(quantity : Number) : boolean` 233 | 234 | Returns true if the Product is currently online (based on its online flag and online dates) and the specified quantity does not exceed the quantity available for sale, and returns false otherwise. 235 | 236 | ### isOrderable 237 | 238 | **Signature:** `isOrderable() : boolean` 239 | 240 | Convenience method for isOrderable(Number). 241 | 242 | ## Method Detail 243 | 244 | ## Method Details 245 | 246 | ### getAvailability 247 | 248 | **Signature:** `getAvailability() : Number` 249 | 250 | **Description:** Returns the availability of the product, which roughly defined is the ratio of the original stock that is still available to sell. The basic formula, if the current site uses an inventory list, is the ATS quantity divided by allocation amount. If the product is not orderable at all this method returns 0. The following specific rules apply for standard products: If inventory lists are in use: If no inventory record exists and the inventory list default-in-stock flag is true this method returns 1. If no inventory record exists the inventory list default-in-stock flag is false this method returns 0. If the product is not available this method returns 0. If the product is perpetually available this method returns 1. Otherwise, this method returns ATS / (allocation + preorderBackorderAllocation). (Values from ProductInventoryRecord.) If inventory lists are not in use the method returns 0. The following rules apply for special product types: For a master product this method returns the average availability of its online variations. For a master product with no online variations this method returns 0. For a master product with own inventory record the rules of the standard products apply. Note: In this case the availability of the variations is not considered. For a product set this method returns the greatest availability of the online products in the set. For a product set with no online products this method returns 0. For a product set with an inventory record the rules of the standard products apply. Note: In this case the availability of the set products is not considered. For a bundle, this method returns the least availability of the bundled products according to their bundled quantity and if it exist also from the bundle inventory record. 251 | 252 | --- 253 | 254 | ### getAvailabilityLevels 255 | 256 | **Signature:** `getAvailabilityLevels(quantity : Number) : ProductAvailabilityLevels` 257 | 258 | **Description:** Returns an instance of ProductAvailabilityLevels, where each available quantity represents a part of the input quantity. This method is typically used to display availability information in the context of a known order quantity, e.g. a shopping cart. For example, if for a given product there are 3 pieces in stock with no pre/backorder handling specified, and the order quantity is 10, then the return instance would have the following state: ProductAvailabilityLevels.getInStock() - 3 ProductAvailabilityLevels.getPreorder() - 0 ProductAvailabilityLevels.getBackorder() - 0 ProductAvailabilityLevels.getNotAvailable() - 7 The following assertions can be made about the state of the returned instance. Between 1 and 3 levels are non-zero. The sum of the levels equals the input quantity. ProductAvailabilityLevels.getPreorder() or ProductAvailabilityLevels.getBackorder() may be available, but not both. Product bundles are handled specially: The availability of product bundles is calculated based on the availability of the bundled products. Therefore, if a bundle contains products that are not in stock, then the bundle itself is not in stock. If all the products in the bundle are on backorder, then the bundle itself is backordered. If a product bundle has its own inventory record, then this record may further limit the availability. If a bundle has no record, then only the records of the bundled products are considered. Product masters and product sets without an own inventory record are handled specially too: The availability is calculated based on the availability of the variants or set products. A product master or product set is in stock as soon as one of its variants or set products is in stock. Each product master or product set availability level reflects the sum of the variant or set product availability levels up to the specified quantity. Product masters or product sets with own inventory record are handled like standard products. The availability of the variants or set products is not considered. (Such an inventory scenario should be avoided.) Offline products are always unavailable and will result in returned levels that are all unavailable. When using Omnichannel Inventory (OCI), future restocks provided by OCI are mapped to AVAILABILITY_STATUS_BACKORDER. For more information, see the comments for ProductInventoryRecord. 259 | 260 | **Parameters:** 261 | 262 | - `quantity`: The quantity to evaluate. 263 | 264 | **Returns:** 265 | 266 | an instance of ProductAvailabilityLevels, which encapsulates the number of items for each relevant availability-status. 267 | 268 | **See Also:** 269 | 270 | ProductAvailabilityLevels 271 | 272 | **Throws:** 273 | 274 | IllegalArgumentException - if the specified quantity is less or equal than zero 275 | 276 | --- 277 | 278 | ### getAvailabilityStatus 279 | 280 | **Signature:** `getAvailabilityStatus() : String` 281 | 282 | **Description:** Returns the availability-status for the minimum-orderable-quantity (MOQ) of the product. The MOQ essentially represents a single orderable unit, and therefore can be represented by a single availability-status. This method is essentially a convenience method. The same information can be retrieved by calling getAvailabilityLevels(Number) with the MOQ of the product as the parameter and then retrieving the single status from the returned map. This method is typically used to display a product's availability in the catalog when the order quantity is not known. 283 | 284 | **Returns:** 285 | 286 | the availability-status. 287 | 288 | --- 289 | 290 | ### getInventoryRecord 291 | 292 | **Signature:** `getInventoryRecord() : ProductInventoryRecord` 293 | 294 | **Description:** Returns the ProductInventoryRecord for the Product associated with this model. 295 | 296 | **Returns:** 297 | 298 | the ProductInventoryRecord or null if there is none. 299 | 300 | --- 301 | 302 | ### getSKUCoverage 303 | 304 | **Signature:** `getSKUCoverage() : Number` 305 | 306 | **Description:** Returns the SKU coverage of the product. The basic formula for a master product is the ratio of online variations that are in stock to the total number of online variations. The following specific rules apply for standard products: If the product is in stock this method returns the availability of the product. If the product is out of stock this method returns 0. The following rules apply for special product types: For a master product this method returns the average SKU coverage of its online variations. For a master product with no online variations this method returns 0. For a product set this method returns the ratio of orderable SKUs in the product set over the total number of online SKUs in the product set. For a product set with no online products this method returns 0. For a product bundle this method returns 1 if all of the bundled products are online, and 0 otherwise. For a product bundle with no online bundled products this method returns 0. 307 | 308 | --- 309 | 310 | ### getTimeToOutOfStock 311 | 312 | **Signature:** `getTimeToOutOfStock() : Number` 313 | 314 | **Description:** Returns the number of hours before the product is expected to go out of stock. The basic formula is the ATS quantity divided by the sales velocity for the most recent day. The following specific rules apply for standard products: If the product is out of stock this method returns 0. If the product is perpetually available this method returns 1. If the sales velocity or ATS is not available this method returns 0. Otherwise this method returns ATS / sales velocity. The following rules apply for special product types: For a master product this method returns the greatest time to out of stock of its online variations. For a master product with no online variations this method returns 0. For a product set this method returns the greatest time to out of stock of the online products in the set. For a product set with no online products this method returns 0. For a bundle with no product inventory record, this method returns the least time to out of stock of the online bundled products. For a bundle with no product inventory record, and no online bundled products, this method returns 0. 315 | 316 | --- 317 | 318 | ### isInStock 319 | 320 | **Signature:** `isInStock(quantity : Number) : boolean` 321 | 322 | **Description:** Returns true if the Product is in-stock in the given quantity. This is determined as follows: If the product is not currently online (based on its online flag and online dates), then return false. If there is no inventory-list for the current site, then return false. If there is no inventory-record for the product, then return the default setting on the inventory-list. If there is no allocation-amount on the inventory-record, then return the value of the perpetual-flag. If there is an allocation-amount, but the perpetual-flag is true, then return true. If the quantity is less than or equal to the stock-level, then return true. Otherwise return false. 323 | 324 | **Parameters:** 325 | 326 | - `quantity`: the quantity that is requested 327 | 328 | **Returns:** 329 | 330 | true if the Product is in-stock. 331 | 332 | **Throws:** 333 | 334 | Exception - if the specified quantity is less or equal than zero 335 | 336 | --- 337 | 338 | ### isInStock 339 | 340 | **Signature:** `isInStock() : boolean` 341 | 342 | **Description:** Convenience method for isInStock(Number). Returns true, if the Product is available in the minimum-order-quantity. If the product does not have a minimum-order-quantity defined, in-stock is checked for a quantity value 1. 343 | 344 | **Returns:** 345 | 346 | true if the Product is in stock, otherwise false. 347 | 348 | --- 349 | 350 | ### isOrderable 351 | 352 | **Signature:** `isOrderable(quantity : Number) : boolean` 353 | 354 | **Description:** Returns true if the Product is currently online (based on its online flag and online dates) and the specified quantity does not exceed the quantity available for sale, and returns false otherwise. Note: Orderable status is more general than in-stock status. A product may be out-of-stock but orderable because it is back-orderable or pre-orderable. 355 | 356 | **Parameters:** 357 | 358 | - `quantity`: the quantity to test against. 359 | 360 | **Returns:** 361 | 362 | true if the item can be ordered in the specified quantity. 363 | 364 | **Throws:** 365 | 366 | Exception - if the specified quantity is less or equal than zero 367 | 368 | --- 369 | 370 | ### isOrderable 371 | 372 | **Signature:** `isOrderable() : boolean` 373 | 374 | **Description:** Convenience method for isOrderable(Number). Returns true if the Product is currently online (based on its online flag and online dates) and is orderable in its minimum-order-quantity. If the product does not have a minimum-order-quantity specified, then 1 is used. The method returns false otherwise. Note: Orderable status is more general than in-stock status. A product may be out-of-stock but orderable because it is back-orderable or pre-orderable. 375 | 376 | **Returns:** 377 | 378 | true if the Product is orderable for the minimum-order-quantity of the product. 379 | 380 | --- ``` -------------------------------------------------------------------------------- /tests/base-handler.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { BaseToolHandler, HandlerContext, ToolArguments, ToolExecutionResult, HandlerError, GenericToolSpec, ToolExecutionContext } from '../src/core/handlers/base-handler.js'; 2 | import { Logger } from '../src/utils/logger.js'; 3 | 4 | // Mock implementation for testing 5 | class TestHandler extends BaseToolHandler { 6 | public initializeCalled = false; 7 | public disposeCalled = false; 8 | public initializeError: Error | null = null; 9 | public disposeError: Error | null = null; 10 | private toolConfig: Record<string, GenericToolSpec> = { 11 | 'test_tool': { 12 | exec: async (args) => `test_tool executed successfully with ${JSON.stringify(args)}`, 13 | logMessage: () => 'Testing tool execution', 14 | }, 15 | 'failing_tool': { 16 | exec: async () => { 17 | throw new Error('Test operation failed'); 18 | }, 19 | logMessage: () => 'Testing failing tool', 20 | }, 21 | 'validate_tool': { 22 | validate: (args, toolName) => { 23 | this.validateArgs(args, ['required_field'], toolName); 24 | }, 25 | exec: async (args) => `validate_tool executed successfully with ${JSON.stringify(args)}`, 26 | logMessage: () => 'Testing validation tool', 27 | }, 28 | 'defaults_tool': { 29 | defaults: (args) => ({ 30 | ...args, 31 | defaultValue: args.defaultValue ?? 'default_applied', 32 | numericDefault: args.numericDefault ?? 42, 33 | }), 34 | exec: async (args) => ({ receivedArgs: args }), 35 | logMessage: (args) => `Defaults tool with ${JSON.stringify(args)}`, 36 | }, 37 | 'context_tool': { 38 | exec: async (args, context) => ({ 39 | hasContext: !!context, 40 | hasHandlerContext: !!context.handlerContext, 41 | hasLogger: !!context.logger, 42 | contextKeys: Object.keys(context), 43 | }), 44 | logMessage: () => 'Testing execution context', 45 | }, 46 | 'complex_validation_tool': { 47 | validate: (args, toolName) => { 48 | if (!args.email?.includes('@')) { 49 | throw new HandlerError('Invalid email format', toolName, 'VALIDATION_ERROR'); 50 | } 51 | if (!args.age || args.age < 18) { 52 | throw new HandlerError('Age must be 18 or older', toolName, 'AGE_VALIDATION_ERROR'); 53 | } 54 | }, 55 | exec: async (args) => ({ validated: true, args }), 56 | logMessage: () => 'Testing complex validation', 57 | }, 58 | }; 59 | 60 | constructor(context: HandlerContext, subLoggerName: string = 'Test') { 61 | super(context, subLoggerName); 62 | } 63 | 64 | protected getToolConfig(): Record<string, GenericToolSpec> { 65 | return this.toolConfig; 66 | } 67 | 68 | protected getToolNameSet(): Set<string> { 69 | return new Set(['test_tool', 'failing_tool', 'validate_tool', 'defaults_tool', 'context_tool', 'complex_validation_tool']); 70 | } 71 | 72 | protected async createExecutionContext(): Promise<ToolExecutionContext> { 73 | return { 74 | handlerContext: this.context, 75 | logger: this.logger, 76 | }; 77 | } 78 | 79 | protected async onInitialize(): Promise<void> { 80 | this.initializeCalled = true; 81 | if (this.initializeError) { 82 | throw this.initializeError; 83 | } 84 | } 85 | 86 | protected async onDispose(): Promise<void> { 87 | this.disposeCalled = true; 88 | if (this.disposeError) { 89 | throw this.disposeError; 90 | } 91 | } 92 | 93 | // Expose protected methods for testing 94 | public testValidateArgs(args: ToolArguments, required: string[], toolName: string): void { 95 | this.validateArgs(args, required, toolName); 96 | } 97 | 98 | public testCreateResponse(data: any, stringify: boolean = true): ToolExecutionResult { 99 | return this.createResponse(data, stringify); 100 | } 101 | 102 | public getIsInitialized(): boolean { 103 | return (this as any)._isInitialized; 104 | } 105 | 106 | public getContext(): HandlerContext { 107 | return this.context; 108 | } 109 | } 110 | 111 | describe('BaseToolHandler', () => { 112 | let mockLogger: jest.Mocked<Logger>; 113 | let context: HandlerContext; 114 | let handler: TestHandler; 115 | 116 | beforeEach(() => { 117 | mockLogger = { 118 | debug: jest.fn(), 119 | log: jest.fn(), 120 | error: jest.fn(), 121 | timing: jest.fn(), 122 | methodEntry: jest.fn(), 123 | methodExit: jest.fn(), 124 | } as any; 125 | 126 | jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger); 127 | 128 | context = { 129 | logger: mockLogger, 130 | config: { hostname: 'test.demandware.net' }, 131 | capabilities: { canAccessLogs: true, canAccessOCAPI: true }, 132 | }; 133 | 134 | handler = new TestHandler(context); 135 | }); 136 | 137 | afterEach(() => { 138 | jest.restoreAllMocks(); 139 | }); 140 | 141 | describe('constructor', () => { 142 | it('should initialize with context and logger', () => { 143 | expect(handler.getContext()).toBe(context); 144 | expect(Logger.getChildLogger).toHaveBeenCalledWith('Handler:Test'); 145 | expect(handler.getIsInitialized()).toBe(false); 146 | }); 147 | }); 148 | 149 | describe('initialization lifecycle', () => { 150 | it('should initialize on first use', async () => { 151 | expect(handler.initializeCalled).toBe(false); 152 | expect(handler.getIsInitialized()).toBe(false); 153 | 154 | const result = await handler.handle('test_tool', {}, Date.now()); 155 | 156 | expect(handler.initializeCalled).toBe(true); 157 | expect(handler.getIsInitialized()).toBe(true); 158 | expect(result.content[0].text).toContain('test_tool executed successfully'); 159 | }); 160 | 161 | it('should not initialize twice', async () => { 162 | // First call 163 | await handler.handle('test_tool', {}, Date.now()); 164 | expect(handler.initializeCalled).toBe(true); 165 | 166 | // Reset the flag to test it's not called again 167 | handler.initializeCalled = false; 168 | 169 | // Second call 170 | await handler.handle('test_tool', {}, Date.now()); 171 | expect(handler.initializeCalled).toBe(false); // Should not be called again 172 | expect(handler.getIsInitialized()).toBe(true); 173 | }); 174 | 175 | it('should handle initialization errors', async () => { 176 | handler.initializeError = new Error('Initialization failed'); 177 | 178 | const result = await handler.handle('test_tool', {}, Date.now()); 179 | 180 | expect(result.isError).toBe(true); 181 | expect(result.content[0].text).toContain('Initialization failed'); 182 | expect(handler.getIsInitialized()).toBe(false); 183 | }); 184 | }); 185 | 186 | describe('disposal lifecycle', () => { 187 | it('should dispose properly', async () => { 188 | // Initialize first 189 | await handler.handle('test_tool', {}, Date.now()); 190 | expect(handler.getIsInitialized()).toBe(true); 191 | 192 | // Dispose 193 | await handler.dispose(); 194 | expect(handler.disposeCalled).toBe(true); 195 | expect(handler.getIsInitialized()).toBe(false); 196 | }); 197 | 198 | it('should handle disposal errors', async () => { 199 | handler.disposeError = new Error('Disposal failed'); 200 | 201 | await expect(handler.dispose()).rejects.toThrow('Disposal failed'); 202 | expect(handler.getIsInitialized()).toBe(false); // Should still reset the flag 203 | }); 204 | 205 | it('should allow disposal without initialization', async () => { 206 | expect(handler.getIsInitialized()).toBe(false); 207 | 208 | await handler.dispose(); 209 | expect(handler.disposeCalled).toBe(true); 210 | expect(handler.getIsInitialized()).toBe(false); 211 | }); 212 | }); 213 | 214 | describe('canHandle', () => { 215 | it('should correctly identify handled tools', () => { 216 | expect(handler.canHandle('test_tool')).toBe(true); 217 | expect(handler.canHandle('failing_tool')).toBe(true); 218 | expect(handler.canHandle('validate_tool')).toBe(true); 219 | expect(handler.canHandle('unknown_tool')).toBe(false); 220 | }); 221 | }); 222 | 223 | describe('handle method', () => { 224 | it('should execute tool successfully', async () => { 225 | const startTime = Date.now(); 226 | const args = { param1: 'value1' }; 227 | 228 | const result = await handler.handle('test_tool', args, startTime); 229 | 230 | expect(result.content[0].text).toContain('test_tool executed successfully'); 231 | expect(result.content[0].text).toContain('value1'); 232 | expect(mockLogger.timing).toHaveBeenCalledWith('test_tool', startTime); 233 | }); 234 | 235 | it('should handle tool execution errors', async () => { 236 | const result = await handler.handle('failing_tool', {}, Date.now()); 237 | 238 | expect(result.isError).toBe(true); 239 | expect(result.content[0].text).toContain('Test operation failed'); 240 | }); 241 | 242 | it('should validate arguments when required', async () => { 243 | const args = { required_field: 'value' }; 244 | 245 | // Should pass with required field 246 | const result = await handler.handle('validate_tool', args, Date.now()); 247 | expect(result.content[0].text).toContain('validate_tool executed successfully'); 248 | 249 | // Should fail without required field 250 | const errorResult = await handler.handle('validate_tool', {}, Date.now()); 251 | expect(errorResult.isError).toBe(true); 252 | expect(errorResult.content[0].text).toContain('required_field is required'); 253 | }); 254 | }); 255 | 256 | describe('validateArgs', () => { 257 | it('should pass when all required fields are present', () => { 258 | const args = { field1: 'value1', field2: 'value2' }; 259 | 260 | expect(() => { 261 | handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); 262 | }).not.toThrow(); 263 | }); 264 | 265 | it('should throw HandlerError when required field is missing', () => { 266 | const args = { field1: 'value1' }; 267 | 268 | expect(() => { 269 | handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); 270 | }).toThrow(HandlerError); 271 | 272 | try { 273 | handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); 274 | } catch (error) { 275 | expect(error).toBeInstanceOf(HandlerError); 276 | expect((error as HandlerError).message).toBe('field2 is required'); 277 | expect((error as HandlerError).toolName).toBe('test_tool'); 278 | expect((error as HandlerError).code).toBe('MISSING_ARGUMENT'); 279 | expect((error as HandlerError).details).toEqual({ 280 | required: ['field1', 'field2'], 281 | provided: ['field1'], 282 | }); 283 | } 284 | }); 285 | 286 | it('should throw when required field is null or undefined', () => { 287 | expect(() => { 288 | handler.testValidateArgs({ field1: null }, ['field1'], 'test_tool'); 289 | }).toThrow('field1 is required'); 290 | 291 | expect(() => { 292 | handler.testValidateArgs({ field1: undefined }, ['field1'], 'test_tool'); 293 | }).toThrow('field1 is required'); 294 | }); 295 | 296 | it('should handle empty args object', () => { 297 | expect(() => { 298 | handler.testValidateArgs({}, ['field1'], 'test_tool'); 299 | }).toThrow('field1 is required'); 300 | }); 301 | 302 | it('should handle null args', () => { 303 | expect(() => { 304 | handler.testValidateArgs(null as any, ['field1'], 'test_tool'); 305 | }).toThrow('field1 is required'); 306 | }); 307 | }); 308 | 309 | describe('createResponse', () => { 310 | it('should create stringified response by default', () => { 311 | const data = { key: 'value', number: 42 }; 312 | const response = handler.testCreateResponse(data); 313 | 314 | expect(response.content[0].text).toBe(JSON.stringify(data, null, 2)); 315 | expect(response.isError).toBe(false); 316 | }); 317 | 318 | it('should create non-stringified response when requested', () => { 319 | const data = { key: 'value', number: 42 }; 320 | const response = handler.testCreateResponse(data, false); 321 | 322 | expect(response.content[0].text).toBe(data); 323 | expect(response.isError).toBe(false); 324 | }); 325 | 326 | it('should handle null data', () => { 327 | const response = handler.testCreateResponse(null); 328 | 329 | expect(response.content[0].text).toBe('null'); 330 | expect(response.isError).toBe(false); 331 | }); 332 | 333 | it('should handle primitive values', () => { 334 | expect(handler.testCreateResponse('string').content[0].text).toBe('"string"'); 335 | expect(handler.testCreateResponse(42).content[0].text).toBe('42'); 336 | expect(handler.testCreateResponse(true).content[0].text).toBe('true'); 337 | }); 338 | }); 339 | 340 | describe('HandlerError', () => { 341 | it('should create error with all properties', () => { 342 | const details = { key: 'value' }; 343 | const error = new HandlerError('Test error', 'test_tool', 'TEST_CODE', details); 344 | 345 | expect(error.message).toBe('Test error'); 346 | expect(error.toolName).toBe('test_tool'); 347 | expect(error.code).toBe('TEST_CODE'); 348 | expect(error.details).toBe(details); 349 | expect(error.name).toBe('HandlerError'); 350 | expect(error).toBeInstanceOf(Error); 351 | }); 352 | 353 | it('should create error with optional parameters', () => { 354 | const error = new HandlerError('Simple error', 'test_tool'); 355 | 356 | expect(error.message).toBe('Simple error'); 357 | expect(error.toolName).toBe('test_tool'); 358 | expect(error.code).toBe('HANDLER_ERROR'); 359 | expect(error.details).toBeUndefined(); 360 | }); 361 | }); 362 | 363 | describe('logging integration', () => { 364 | it('should log debug messages during execution', async () => { 365 | await handler.handle('test_tool', {}, Date.now()); 366 | 367 | expect(mockLogger.debug).toHaveBeenCalledWith( 368 | 'test_tool completed successfully', 369 | expect.any(Object), 370 | ); 371 | }); 372 | 373 | it('should log timing information', async () => { 374 | const startTime = Date.now(); 375 | await handler.handle('test_tool', {}, startTime); 376 | 377 | expect(mockLogger.timing).toHaveBeenCalledWith('test_tool', startTime); 378 | }); 379 | }); 380 | 381 | describe('error handling', () => { 382 | it('should preserve original error types', async () => { 383 | const customError = new TypeError('Custom type error'); 384 | handler.initializeError = customError; 385 | 386 | const result = await handler.handle('test_tool', {}, Date.now()); 387 | expect(result.isError).toBe(true); 388 | expect(result.content[0].text).toContain('Custom type error'); 389 | }); 390 | 391 | it('should handle async operation errors', async () => { 392 | const result = await handler.handle('failing_tool', {}, Date.now()); 393 | expect(result.isError).toBe(true); 394 | expect(result.content[0].text).toContain('Test operation failed'); 395 | }); 396 | }); 397 | 398 | describe('config-driven functionality', () => { 399 | describe('unsupported tools', () => { 400 | it('should throw error for unsupported tools', async () => { 401 | await expect(handler.handle('unknown_tool', {}, Date.now())) 402 | .rejects.toThrow('Unsupported tool: unknown_tool'); 403 | }); 404 | 405 | it('should return false for canHandle on unsupported tools', () => { 406 | expect(handler.canHandle('unknown_tool')).toBe(false); 407 | }); 408 | }); 409 | 410 | describe('default values', () => { 411 | it('should apply default values when not provided', async () => { 412 | const result = await handler.handle('defaults_tool', {}, Date.now()); 413 | const parsedResult = JSON.parse(result.content[0].text); 414 | 415 | expect(parsedResult.receivedArgs.defaultValue).toBe('default_applied'); 416 | expect(parsedResult.receivedArgs.numericDefault).toBe(42); 417 | }); 418 | 419 | it('should not override provided values with defaults', async () => { 420 | const args = { defaultValue: 'custom_value', numericDefault: 100 }; 421 | const result = await handler.handle('defaults_tool', args, Date.now()); 422 | const parsedResult = JSON.parse(result.content[0].text); 423 | 424 | expect(parsedResult.receivedArgs.defaultValue).toBe('custom_value'); 425 | expect(parsedResult.receivedArgs.numericDefault).toBe(100); 426 | }); 427 | 428 | it('should mix provided and default values', async () => { 429 | const args = { defaultValue: 'custom_value', otherParam: 'other' }; 430 | const result = await handler.handle('defaults_tool', args, Date.now()); 431 | const parsedResult = JSON.parse(result.content[0].text); 432 | 433 | expect(parsedResult.receivedArgs.defaultValue).toBe('custom_value'); 434 | expect(parsedResult.receivedArgs.numericDefault).toBe(42); // default applied 435 | expect(parsedResult.receivedArgs.otherParam).toBe('other'); 436 | }); 437 | }); 438 | 439 | describe('execution context', () => { 440 | it('should provide ToolExecutionContext to tool functions', async () => { 441 | const result = await handler.handle('context_tool', {}, Date.now()); 442 | const parsedResult = JSON.parse(result.content[0].text); 443 | 444 | expect(parsedResult.hasContext).toBe(true); 445 | expect(parsedResult.hasHandlerContext).toBe(true); 446 | expect(parsedResult.hasLogger).toBe(true); 447 | expect(parsedResult.contextKeys).toContain('handlerContext'); 448 | expect(parsedResult.contextKeys).toContain('logger'); 449 | }); 450 | }); 451 | 452 | describe('complex validation', () => { 453 | it('should pass complex validation with valid args', async () => { 454 | const args = { email: '[email protected]', age: 25 }; 455 | const result = await handler.handle('complex_validation_tool', args, Date.now()); 456 | const parsedResult = JSON.parse(result.content[0].text); 457 | 458 | expect(parsedResult.validated).toBe(true); 459 | expect(parsedResult.args).toEqual(args); 460 | }); 461 | 462 | it('should fail validation with invalid email', async () => { 463 | const args = { email: 'invalid-email', age: 25 }; 464 | const result = await handler.handle('complex_validation_tool', args, Date.now()); 465 | 466 | expect(result.isError).toBe(true); 467 | expect(result.content[0].text).toContain('Invalid email format'); 468 | }); 469 | 470 | it('should fail validation with invalid age', async () => { 471 | const args = { email: '[email protected]', age: 16 }; 472 | const result = await handler.handle('complex_validation_tool', args, Date.now()); 473 | 474 | expect(result.isError).toBe(true); 475 | expect(result.content[0].text).toContain('Age must be 18 or older'); 476 | }); 477 | 478 | it('should handle validation errors with custom error codes', async () => { 479 | const args = { email: 'invalid-email', age: 25 }; 480 | const result = await handler.handle('complex_validation_tool', args, Date.now()); 481 | 482 | expect(result.isError).toBe(true); 483 | expect(result.content[0].text).toContain('Invalid email format'); 484 | // The error should be a HandlerError with VALIDATION_ERROR code 485 | }); 486 | }); 487 | 488 | describe('logging with defaults', () => { 489 | it('should use log message with applied defaults', async () => { 490 | const args = { customParam: 'test' }; 491 | 492 | // Spy on the debug method to capture log messages 493 | const debugSpy = jest.spyOn(mockLogger, 'debug'); 494 | 495 | await handler.handle('defaults_tool', args, Date.now()); 496 | 497 | // Check that the log message includes the default values 498 | const debugCalls = debugSpy.mock.calls; 499 | const logMessageCall = debugCalls.find(call => 500 | typeof call[0] === 'string' && call[0].includes('Defaults tool with'), 501 | ); 502 | 503 | expect(logMessageCall).toBeDefined(); 504 | expect(logMessageCall?.[0]).toContain('default_applied'); 505 | expect(logMessageCall?.[0]).toContain('42'); 506 | }); 507 | }); 508 | 509 | describe('tool config edge cases', () => { 510 | it('should handle tools with minimal config', async () => { 511 | // The test_tool has minimal config - no validation, no defaults 512 | const result = await handler.handle('test_tool', { param: 'value' }, Date.now()); 513 | 514 | expect(result.content[0].text).toContain('test_tool executed successfully'); 515 | expect(result.content[0].text).toContain('value'); 516 | }); 517 | 518 | it('should handle empty arguments with defaults', async () => { 519 | const result = await handler.handle('defaults_tool', {}, Date.now()); 520 | const parsedResult = JSON.parse(result.content[0].text); 521 | 522 | expect(parsedResult.receivedArgs.defaultValue).toBe('default_applied'); 523 | expect(parsedResult.receivedArgs.numericDefault).toBe(42); 524 | }); 525 | }); 526 | }); 527 | }); 528 | ``` -------------------------------------------------------------------------------- /docs/dw_crypto/WeakCipher.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.crypto 2 | 3 | # Class WeakCipher 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.crypto.WeakCipher 9 | 10 | ## Description 11 | 12 | This API provides access to Deprecated algorithms. See Cipher for full documentation. WeakCipher is simply a drop-in replacement that only supports deprecated algorithms and key lengths. This is helpful when you need to deal with weak algorithms for backward compatibility purposes, but Cipher should always be used for new development and for anything intended to be secure. Note: this class handles sensitive security-related data. Pay special attention to PCI DSS v3 requirements 2, 4, and 12. 13 | 14 | ## Constants 15 | 16 | ### CHAR_ENCODING 17 | 18 | **Type:** String = "UTF8" 19 | 20 | Strings containing keys, plain texts, cipher texts etc. are internally converted into byte arrays using this encoding (currently UTF8). 21 | 22 | ## Properties 23 | 24 | ## Constructor Summary 25 | 26 | WeakCipher() 27 | 28 | ## Method Summary 29 | 30 | ### decrypt 31 | 32 | **Signature:** `decrypt(base64Msg : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` 33 | 34 | Decrypts the message using the given parameters. 35 | 36 | ### decrypt 37 | 38 | **Signature:** `decrypt(base64Msg : String, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : String` 39 | 40 | Alternative method to decrypt(String, String, String, String, Number), which allows using a key in the keystore for the decryption. 41 | 42 | ### decrypt 43 | 44 | **Signature:** `decrypt(base64Msg : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` 45 | 46 | Decrypts the message using the given parameters. 47 | 48 | ### decrypt 49 | 50 | **Signature:** `decrypt(base64Msg : String, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : String` 51 | 52 | Alternative method to decrypt_3(String, String, String, String, Number), which allows using a key in the keystore for the decryption. 53 | 54 | ### decryptBytes 55 | 56 | **Signature:** `decryptBytes(encryptedBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 57 | 58 | Lower-level decryption API. 59 | 60 | ### decryptBytes 61 | 62 | **Signature:** `decryptBytes(encryptedBytes : Bytes, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 63 | 64 | Alternative method to decryptBytes(Bytes, String, String, String, Number), which allows to use a key in the keystore for the decryption. 65 | 66 | ### decryptBytes 67 | 68 | **Signature:** `decryptBytes(encryptedBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 69 | 70 | Lower-level decryption API. 71 | 72 | ### decryptBytes 73 | 74 | **Signature:** `decryptBytes(encryptedBytes : Bytes, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 75 | 76 | Alternative method to decryptBytes_3(Bytes, String, String, String, Number), which allows to use a key in the keystore for the decryption. 77 | 78 | ### encrypt 79 | 80 | **Signature:** `encrypt(message : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` 81 | 82 | Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. 83 | 84 | ### encrypt 85 | 86 | **Signature:** `encrypt(message : String, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : String` 87 | 88 | Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. 89 | 90 | ### encrypt 91 | 92 | **Signature:** `encrypt(message : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` 93 | 94 | Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. 95 | 96 | ### encrypt 97 | 98 | **Signature:** `encrypt(message : String, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : String` 99 | 100 | Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. 101 | 102 | ### encryptBytes 103 | 104 | **Signature:** `encryptBytes(messageBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 105 | 106 | Lower-level encryption API. 107 | 108 | ### encryptBytes 109 | 110 | **Signature:** `encryptBytes(messageBytes : Bytes, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 111 | 112 | Alternative method to encryptBytes(Bytes, String, String, String, Number), which allows to use a key in the keystore for the encryption. 113 | 114 | ### encryptBytes 115 | 116 | **Signature:** `encryptBytes(messageBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 117 | 118 | Lower-level encryption API. 119 | 120 | ### encryptBytes 121 | 122 | **Signature:** `encryptBytes(messageBytes : Bytes, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 123 | 124 | Alternative method to encryptBytes_3(Bytes, String, String, String, Number), which allows to use a key in the keystore for the encryption. 125 | 126 | ## Constructor Detail 127 | 128 | ## Method Detail 129 | 130 | ## Method Details 131 | 132 | ### decrypt 133 | 134 | **Signature:** `decrypt(base64Msg : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` 135 | 136 | **Description:** Decrypts the message using the given parameters. See Cipher.decrypt(String, String, String, String, Number) for full documentation. 137 | 138 | **API Versioned:** 139 | 140 | From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. 141 | 142 | **Parameters:** 143 | 144 | - `base64Msg`: the base64 encoded data to decrypt 145 | - `key`: The decryption key 146 | - `transformation`: Transformation in "algorithm/mode/padding" format. 147 | - `saltOrIV`: Initialization value appropriate for the algorithm. 148 | - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable 149 | 150 | **Returns:** 151 | 152 | the original plaintext message. 153 | 154 | --- 155 | 156 | ### decrypt 157 | 158 | **Signature:** `decrypt(base64Msg : String, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : String` 159 | 160 | **Description:** Alternative method to decrypt(String, String, String, String, Number), which allows using a key in the keystore for the decryption. See Cipher.decrypt(String, KeyRef, String, String, Number) for full documentation. 161 | 162 | **API Versioned:** 163 | 164 | From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. 165 | 166 | **Parameters:** 167 | 168 | - `base64Msg`: the base64 encoded data to decrypt 169 | - `privateKey`: A reference to a private key in the key store. 170 | - `transformation`: Transformation in "algorithm/mode/padding" format. 171 | - `saltOrIV`: Initialization value appropriate for the algorithm. 172 | - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable 173 | 174 | **Returns:** 175 | 176 | the original plaintext message. 177 | 178 | --- 179 | 180 | ### decrypt 181 | 182 | **Signature:** `decrypt(base64Msg : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` 183 | 184 | **Description:** Decrypts the message using the given parameters. See Cipher.decrypt_3(String, String, String, String, Number) for full documentation. 185 | 186 | **API Versioned:** 187 | 188 | From version 16.2. Does not use a default initialization vector. 189 | 190 | **Parameters:** 191 | 192 | - `base64Msg`: the base64 encoded data to decrypt 193 | - `key`: The decryption key 194 | - `transformation`: Transformation in "algorithm/mode/padding" format. 195 | - `saltOrIV`: Initialization value appropriate for the algorithm. 196 | - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable 197 | 198 | **Returns:** 199 | 200 | the original plaintext message. 201 | 202 | --- 203 | 204 | ### decrypt 205 | 206 | **Signature:** `decrypt(base64Msg : String, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : String` 207 | 208 | **Description:** Alternative method to decrypt_3(String, String, String, String, Number), which allows using a key in the keystore for the decryption. See Cipher.decrypt_3(String, KeyRef, String, String, Number) for full documentation. 209 | 210 | **API Versioned:** 211 | 212 | From version 16.2. Does not use a default initialization vector. 213 | 214 | **Parameters:** 215 | 216 | - `base64Msg`: the base64 encoded data to decrypt 217 | - `privateKey`: A reference to a private key in the key store. 218 | - `transformation`: Transformation in "algorithm/mode/padding" format. 219 | - `saltOrIV`: Initialization value appropriate for the algorithm. 220 | - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable 221 | 222 | **Returns:** 223 | 224 | the original plaintext message. 225 | 226 | --- 227 | 228 | ### decryptBytes 229 | 230 | **Signature:** `decryptBytes(encryptedBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 231 | 232 | **Description:** Lower-level decryption API. Decrypts the passed bytes using the specified key and applying the transformations described by the specified parameters. See Cipher.decryptBytes(Bytes, String, String, String, Number) for full documentation. 233 | 234 | **API Versioned:** 235 | 236 | From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. 237 | 238 | **Parameters:** 239 | 240 | - `encryptedBytes`: The bytes to decrypt. 241 | - `key`: The key to use for decryption. 242 | - `transformation`: The transformation used to originally encrypt. 243 | - `saltOrIV`: the salt or IV to use. 244 | - `iterations`: the iterations to use. 245 | 246 | **Returns:** 247 | 248 | The decrypted bytes. 249 | 250 | **See Also:** 251 | 252 | decrypt(String, String, String, String, Number) 253 | 254 | --- 255 | 256 | ### decryptBytes 257 | 258 | **Signature:** `decryptBytes(encryptedBytes : Bytes, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 259 | 260 | **Description:** Alternative method to decryptBytes(Bytes, String, String, String, Number), which allows to use a key in the keystore for the decryption. See Cipher.decryptBytes(Bytes, KeyRef, String, String, Number) for full documentation. 261 | 262 | **API Versioned:** 263 | 264 | From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. 265 | 266 | **Parameters:** 267 | 268 | - `encryptedBytes`: The bytes to decrypt. 269 | - `privateKey`: A reference to a private key in the key store. 270 | - `transformation`: The transformation used to originally encrypt. 271 | - `saltOrIV`: the salt or IV to use. 272 | - `iterations`: the iterations to use. 273 | 274 | **Returns:** 275 | 276 | The decrypted bytes. 277 | 278 | --- 279 | 280 | ### decryptBytes 281 | 282 | **Signature:** `decryptBytes(encryptedBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 283 | 284 | **Description:** Lower-level decryption API. Decrypts the passed bytes using the specified key and applying the transformations described by the specified parameters. See Cipher.decryptBytes_3(Bytes, String, String, String, Number) for full documentation. 285 | 286 | **API Versioned:** 287 | 288 | From version 16.2. Does not use a default initialization vector. 289 | 290 | **Parameters:** 291 | 292 | - `encryptedBytes`: The bytes to decrypt. 293 | - `key`: The key to use for decryption. 294 | - `transformation`: The transformation used to originally encrypt. 295 | - `saltOrIV`: the salt or IV to use. 296 | - `iterations`: the iterations to use. 297 | 298 | **Returns:** 299 | 300 | The decrypted bytes. 301 | 302 | **See Also:** 303 | 304 | decrypt_3(String, String, String, String, Number) 305 | 306 | --- 307 | 308 | ### decryptBytes 309 | 310 | **Signature:** `decryptBytes(encryptedBytes : Bytes, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 311 | 312 | **Description:** Alternative method to decryptBytes_3(Bytes, String, String, String, Number), which allows to use a key in the keystore for the decryption. See Cipher.decryptBytes_3(Bytes, KeyRef, String, String, Number) for full documentation. 313 | 314 | **API Versioned:** 315 | 316 | From version 16.2. Does not use a default initialization vector. 317 | 318 | **Parameters:** 319 | 320 | - `encryptedBytes`: The bytes to decrypt. 321 | - `privateKey`: A reference to a private key in the key store. 322 | - `transformation`: The transformation used to originally encrypt. 323 | - `saltOrIV`: the salt or IV to use. 324 | - `iterations`: the iterations to use. 325 | 326 | **Returns:** 327 | 328 | The decrypted bytes. 329 | 330 | --- 331 | 332 | ### encrypt 333 | 334 | **Signature:** `encrypt(message : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` 335 | 336 | **Description:** Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. See Cipher.encrypt(String, String, String, String, Number) for full documentation. 337 | 338 | **API Versioned:** 339 | 340 | From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. 341 | 342 | **Parameters:** 343 | 344 | - `message`: Message to encrypt (this will be converted to UTF-8 first) 345 | - `key`: Key 346 | - `transformation`: Transformation in "algorithm/mode/padding" format 347 | - `saltOrIV`: Initialization value appropriate for the algorithm 348 | - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable 349 | 350 | **Returns:** 351 | 352 | Base64-encoded encrypted data 353 | 354 | --- 355 | 356 | ### encrypt 357 | 358 | **Signature:** `encrypt(message : String, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : String` 359 | 360 | **Description:** Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. See Cipher.encrypt(String, CertificateRef, String, String, Number) for full documentation. 361 | 362 | **API Versioned:** 363 | 364 | From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. 365 | 366 | **Parameters:** 367 | 368 | - `message`: Message to encrypt (this will be converted to UTF-8 first) 369 | - `publicKey`: A reference to a public key 370 | - `transformation`: Transformation in "algorithm/mode/padding" format 371 | - `saltOrIV`: Initialization value appropriate for the algorithm 372 | - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable 373 | 374 | **Returns:** 375 | 376 | Base64-encoded encrypted data 377 | 378 | --- 379 | 380 | ### encrypt 381 | 382 | **Signature:** `encrypt(message : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` 383 | 384 | **Description:** Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. See Cipher.encrypt_3(String, String, String, String, Number) for full documentation. 385 | 386 | **API Versioned:** 387 | 388 | From version 16.2. Does not use a default initialization vector. 389 | 390 | **Parameters:** 391 | 392 | - `message`: Message to encrypt (this will be converted to UTF-8 first) 393 | - `key`: Key 394 | - `transformation`: Transformation in "algorithm/mode/padding" format 395 | - `saltOrIV`: Initialization value appropriate for the algorithm 396 | - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable 397 | 398 | **Returns:** 399 | 400 | Base64-encoded encrypted data 401 | 402 | --- 403 | 404 | ### encrypt 405 | 406 | **Signature:** `encrypt(message : String, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : String` 407 | 408 | **Description:** Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. See Cipher.encrypt_3(String, CertificateRef, String, String, Number) for full documentation. 409 | 410 | **API Versioned:** 411 | 412 | From version 16.2. Does not use a default initialization vector. 413 | 414 | **Parameters:** 415 | 416 | - `message`: Message to encrypt (this will be converted to UTF-8 first) 417 | - `publicKey`: A reference to a public key 418 | - `transformation`: Transformation in "algorithm/mode/padding" format 419 | - `saltOrIV`: Initialization value appropriate for the algorithm 420 | - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable 421 | 422 | **Returns:** 423 | 424 | Base64-encoded encrypted data 425 | 426 | --- 427 | 428 | ### encryptBytes 429 | 430 | **Signature:** `encryptBytes(messageBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 431 | 432 | **Description:** Lower-level encryption API. Encrypts the passed bytes by using the specified key and applying the transformations described by the specified parameters. See Cipher.encryptBytes(Bytes, String, String, String, Number) for full documentation. 433 | 434 | **API Versioned:** 435 | 436 | From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. 437 | 438 | **Parameters:** 439 | 440 | - `messageBytes`: The bytes to encrypt. 441 | - `key`: The key to use for encryption. 442 | - `transformation`: Transformation in "algorithm/mode/padding" format. 443 | - `saltOrIV`: Initialization value appropriate for the algorithm. 444 | - `iterations`: The number of passes to make when turning a passphrase into a key. 445 | 446 | **Returns:** 447 | 448 | the encrypted bytes. 449 | 450 | **See Also:** 451 | 452 | encrypt(String, String, String, String, Number) 453 | 454 | --- 455 | 456 | ### encryptBytes 457 | 458 | **Signature:** `encryptBytes(messageBytes : Bytes, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 459 | 460 | **Description:** Alternative method to encryptBytes(Bytes, String, String, String, Number), which allows to use a key in the keystore for the encryption. See Cipher.encryptBytes(Bytes, CertificateRef, String, String, Number) for full documentation. 461 | 462 | **API Versioned:** 463 | 464 | From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. 465 | 466 | **Parameters:** 467 | 468 | - `messageBytes`: The bytes to encrypt. 469 | - `publicKey`: A reference to a public key. 470 | - `transformation`: Transformation in "algorithm/mode/padding" format. 471 | - `saltOrIV`: Initialization value appropriate for the algorithm. 472 | - `iterations`: The number of passes to make when turning a passphrase into a key. 473 | 474 | **Returns:** 475 | 476 | the encrypted bytes. 477 | 478 | **See Also:** 479 | 480 | encrypt(String, CertificateRef, String, String, Number) 481 | 482 | --- 483 | 484 | ### encryptBytes 485 | 486 | **Signature:** `encryptBytes(messageBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 487 | 488 | **Description:** Lower-level encryption API. Encrypts the passed bytes by using the specified key and applying the transformations described by the specified parameters. See Cipher.encryptBytes_3(Bytes, String, String, String, Number) for full documentation. 489 | 490 | **API Versioned:** 491 | 492 | From version 16.2. Does not use a default initialization vector. 493 | 494 | **Parameters:** 495 | 496 | - `messageBytes`: The bytes to encrypt. 497 | - `key`: The key to use for encryption. 498 | - `transformation`: Transformation in "algorithm/mode/padding" format. 499 | - `saltOrIV`: Initialization value appropriate for the algorithm. 500 | - `iterations`: The number of passes to make when turning a passphrase into a key. 501 | 502 | **Returns:** 503 | 504 | the encrypted bytes. 505 | 506 | **See Also:** 507 | 508 | encrypt_3(String, String, String, String, Number) 509 | 510 | --- 511 | 512 | ### encryptBytes 513 | 514 | **Signature:** `encryptBytes(messageBytes : Bytes, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` 515 | 516 | **Description:** Alternative method to encryptBytes_3(Bytes, String, String, String, Number), which allows to use a key in the keystore for the encryption. See Cipher.encryptBytes_3(Bytes, CertificateRef, String, String, Number) for full documentation. 517 | 518 | **API Versioned:** 519 | 520 | From version 16.2. Does not use a default initialization vector. 521 | 522 | **Parameters:** 523 | 524 | - `messageBytes`: The bytes to encrypt. 525 | - `publicKey`: A reference to a public key. 526 | - `transformation`: Transformation in "algorithm/mode/padding" format. 527 | - `saltOrIV`: Initialization value appropriate for the algorithm. 528 | - `iterations`: The number of passes to make when turning a passphrase into a key. 529 | 530 | **Returns:** 531 | 532 | the encrypted bytes. 533 | 534 | **See Also:** 535 | 536 | encrypt_3(String, CertificateRef, String, String, Number) 537 | 538 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/search-system-object-attribute-groups.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | # ================================================================================== 2 | # SFCC MCP Server - search_system_object_attribute_groups Tool YAML Tests (Full Mode) 3 | # Streamlined smoke testing and declarative validation for core functionality 4 | # Complex business logic, edge cases, and workflows are covered in programmatic tests 5 | # 6 | # Available System Objects for Attribute Group Testing: 7 | # - Product (35 groups: ChannelIntegration, ExternalSearch, Order, PXL3_Hoesjes, etc.) 8 | # - SitePreferences (8 groups: CCV, SFRA Unified Feature Cartridge, Storefront Configs, etc.) 9 | # - Customer, Order, Category, etc. (varying group counts) 10 | # 11 | # Quick Test Commands: 12 | # aegis "tests/mcp/yaml/search-system-object-attribute-groups.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --verbose 13 | # aegis query search_system_object_attribute_groups '{"objectType": "Product", "searchRequest": {"query": {"match_all_query": {}}, "count": 3}}' --config "aegis.config.with-dw.json" 14 | # aegis query search_system_object_attribute_groups '{"objectType": "SitePreferences", "searchRequest": {"query": {"match_all_query": {}}, "count": 3}}' --config "aegis.config.with-dw.json" 15 | # ================================================================================== 16 | 17 | description: "search_system_object_attribute_groups tool smoke tests - Basic functionality validation" 18 | 19 | tests: 20 | # ================================================================================== 21 | # TOOL AVAILABILITY VALIDATION 22 | # ================================================================================== 23 | - it: "should have search_system_object_attribute_groups tool available with proper schema" 24 | request: 25 | jsonrpc: "2.0" 26 | id: "tool-available" 27 | method: "tools/list" 28 | params: {} 29 | expect: 30 | response: 31 | jsonrpc: "2.0" 32 | id: "tool-available" 33 | result: 34 | tools: 35 | match:arrayElements: 36 | match:partial: 37 | name: "match:type:string" 38 | description: "match:type:string" 39 | match:extractField: "tools.*.name" 40 | value: "match:arrayContains:search_system_object_attribute_groups" 41 | stderr: "toBeEmpty" 42 | 43 | # ================================================================================== 44 | # CORE FUNCTIONALITY VALIDATION - Product Object Type 45 | # ================================================================================== 46 | - it: "should successfully search Product attribute groups with match_all_query and return valid structure" 47 | request: 48 | jsonrpc: "2.0" 49 | id: "product-match-all-success" 50 | method: "tools/call" 51 | params: 52 | name: "search_system_object_attribute_groups" 53 | arguments: 54 | objectType: "Product" 55 | searchRequest: 56 | query: 57 | match_all_query: {} 58 | count: 5 59 | expect: 60 | response: 61 | jsonrpc: "2.0" 62 | id: "product-match-all-success" 63 | result: 64 | content: 65 | - type: "text" 66 | text: "match:contains:object_attribute_group_search_result" 67 | isError: false 68 | performance: 69 | maxResponseTime: "2000ms" 70 | stderr: "toBeEmpty" 71 | 72 | - it: "should return valid JSON structure with attribute group data for Product" 73 | request: 74 | jsonrpc: "2.0" 75 | id: "product-structure" 76 | method: "tools/call" 77 | params: 78 | name: "search_system_object_attribute_groups" 79 | arguments: 80 | objectType: "Product" 81 | searchRequest: 82 | query: 83 | match_all_query: {} 84 | count: 3 85 | expect: 86 | response: 87 | jsonrpc: "2.0" 88 | id: "product-structure" 89 | result: 90 | content: 91 | - type: "text" 92 | text: "match:regex:[\\s\\S]*\"_type\"[\\s\\S]*\"object_attribute_group_search_result\"[\\s\\S]*" 93 | isError: false 94 | stderr: "toBeEmpty" 95 | 96 | - it: "should include pagination metadata and query echo in Product search response" 97 | request: 98 | jsonrpc: "2.0" 99 | id: "product-pagination" 100 | method: "tools/call" 101 | params: 102 | name: "search_system_object_attribute_groups" 103 | arguments: 104 | objectType: "Product" 105 | searchRequest: 106 | query: 107 | match_all_query: {} 108 | count: 2 109 | expect: 110 | response: 111 | jsonrpc: "2.0" 112 | id: "product-pagination" 113 | result: 114 | content: 115 | - type: "text" 116 | text: "match:contains:\"query\"" 117 | isError: false 118 | stderr: "toBeEmpty" 119 | 120 | - it: "should return attribute groups with id and link fields for Product" 121 | request: 122 | jsonrpc: "2.0" 123 | id: "product-hits-structure" 124 | method: "tools/call" 125 | params: 126 | name: "search_system_object_attribute_groups" 127 | arguments: 128 | objectType: "Product" 129 | searchRequest: 130 | query: 131 | match_all_query: {} 132 | count: 2 133 | expect: 134 | response: 135 | jsonrpc: "2.0" 136 | id: "product-hits-structure" 137 | result: 138 | content: 139 | - type: "text" 140 | text: "match:regex:[\\s\\S]*\"hits\"[\\s\\S]*\"id\"[\\s\\S]*\"link\"[\\s\\S]*object_attribute_group[\\s\\S]*" 141 | isError: false 142 | stderr: "toBeEmpty" 143 | 144 | # ================================================================================== 145 | # SITE PREFERENCES FUNCTIONALITY - Essential for Site Preference Discovery 146 | # ================================================================================== 147 | - it: "should successfully search SitePreferences attribute groups (for site preference discovery)" 148 | request: 149 | jsonrpc: "2.0" 150 | id: "siteprefs-success" 151 | method: "tools/call" 152 | params: 153 | name: "search_system_object_attribute_groups" 154 | arguments: 155 | objectType: "SitePreferences" 156 | searchRequest: 157 | query: 158 | match_all_query: {} 159 | count: 3 160 | expect: 161 | response: 162 | jsonrpc: "2.0" 163 | id: "siteprefs-success" 164 | result: 165 | content: 166 | - type: "text" 167 | text: "match:contains:object_attribute_group_search_result" 168 | isError: false 169 | performance: 170 | maxResponseTime: "2000ms" 171 | stderr: "toBeEmpty" 172 | 173 | - it: "should return SitePreferences groups containing expected groups like CCV or Storefront" 174 | request: 175 | jsonrpc: "2.0" 176 | id: "siteprefs-content" 177 | method: "tools/call" 178 | params: 179 | name: "search_system_object_attribute_groups" 180 | arguments: 181 | objectType: "SitePreferences" 182 | searchRequest: 183 | query: 184 | match_all_query: {} 185 | count: 5 186 | expect: 187 | response: 188 | jsonrpc: "2.0" 189 | id: "siteprefs-content" 190 | result: 191 | content: 192 | - type: "text" 193 | text: "match:regex:[\\s\\S]*\"id\".*\"(?:CCV|Storefront|SFRA)[\\s\\S]*" 194 | isError: false 195 | stderr: "toBeEmpty" 196 | 197 | # ================================================================================== 198 | # TEXT SEARCH FUNCTIONALITY VALIDATION 199 | # ================================================================================== 200 | - it: "should support text_query search on id field" 201 | request: 202 | jsonrpc: "2.0" 203 | id: "text-search-id" 204 | method: "tools/call" 205 | params: 206 | name: "search_system_object_attribute_groups" 207 | arguments: 208 | objectType: "Product" 209 | searchRequest: 210 | query: 211 | text_query: 212 | fields: ["id"] 213 | search_phrase: "Order" 214 | count: 3 215 | expect: 216 | response: 217 | jsonrpc: "2.0" 218 | id: "text-search-id" 219 | result: 220 | content: 221 | - type: "text" 222 | text: "match:contains:object_attribute_group_search_result" 223 | isError: false 224 | performance: 225 | maxResponseTime: "2000ms" 226 | stderr: "toBeEmpty" 227 | 228 | - it: "should include the text_query in response echo for text search" 229 | request: 230 | jsonrpc: "2.0" 231 | id: "text-search-echo" 232 | method: "tools/call" 233 | params: 234 | name: "search_system_object_attribute_groups" 235 | arguments: 236 | objectType: "Product" 237 | searchRequest: 238 | query: 239 | text_query: 240 | fields: ["id", "display_name"] 241 | search_phrase: "External" 242 | count: 2 243 | expect: 244 | response: 245 | jsonrpc: "2.0" 246 | id: "text-search-echo" 247 | result: 248 | content: 249 | - type: "text" 250 | text: "match:regex:[\\s\\S]*\"text_query\"[\\s\\S]*\"fields\"[\\s\\S]*\"search_phrase\"[\\s\\S]*\"External\"[\\s\\S]*" 251 | isError: false 252 | stderr: "toBeEmpty" 253 | 254 | # ================================================================================== 255 | # PAGINATION AND SORTING VALIDATION 256 | # ================================================================================== 257 | - it: "should support custom count parameter for pagination" 258 | request: 259 | jsonrpc: "2.0" 260 | id: "custom-count" 261 | method: "tools/call" 262 | params: 263 | name: "search_system_object_attribute_groups" 264 | arguments: 265 | objectType: "Product" 266 | searchRequest: 267 | query: 268 | match_all_query: {} 269 | count: 2 270 | expect: 271 | response: 272 | jsonrpc: "2.0" 273 | id: "custom-count" 274 | result: 275 | content: 276 | - type: "text" 277 | text: "match:regex:[\\s\\S]*\"count\":\\s*2[\\s\\S]*" 278 | isError: false 279 | stderr: "toBeEmpty" 280 | 281 | - it: "should support start parameter for pagination offset" 282 | request: 283 | jsonrpc: "2.0" 284 | id: "pagination-start" 285 | method: "tools/call" 286 | params: 287 | name: "search_system_object_attribute_groups" 288 | arguments: 289 | objectType: "Product" 290 | searchRequest: 291 | query: 292 | match_all_query: {} 293 | start: 2 294 | count: 3 295 | expect: 296 | response: 297 | jsonrpc: "2.0" 298 | id: "pagination-start" 299 | result: 300 | content: 301 | - type: "text" 302 | text: "match:regex:[\\s\\S]*\"start\":\\s*2[\\s\\S]*" 303 | isError: false 304 | stderr: "toBeEmpty" 305 | 306 | - it: "should support sorting by id field in ascending order" 307 | request: 308 | jsonrpc: "2.0" 309 | id: "sorting-id-asc" 310 | method: "tools/call" 311 | params: 312 | name: "search_system_object_attribute_groups" 313 | arguments: 314 | objectType: "Product" 315 | searchRequest: 316 | query: 317 | match_all_query: {} 318 | sorts: 319 | - field: "id" 320 | sort_order: "asc" 321 | count: 3 322 | expect: 323 | response: 324 | jsonrpc: "2.0" 325 | id: "sorting-id-asc" 326 | result: 327 | content: 328 | - type: "text" 329 | text: "match:contains:object_attribute_group_search_result" 330 | isError: false 331 | performance: 332 | maxResponseTime: "2000ms" 333 | stderr: "toBeEmpty" 334 | 335 | # ================================================================================== 336 | # PARAMETER VALIDATION AND ERROR HANDLING 337 | # ================================================================================== 338 | - it: "should reject missing objectType parameter with validation error" 339 | request: 340 | jsonrpc: "2.0" 341 | id: "missing-object-type" 342 | method: "tools/call" 343 | params: 344 | name: "search_system_object_attribute_groups" 345 | arguments: 346 | searchRequest: 347 | query: 348 | match_all_query: {} 349 | count: 3 350 | expect: 351 | response: 352 | jsonrpc: "2.0" 353 | id: "missing-object-type" 354 | result: 355 | content: 356 | - type: "text" 357 | text: "match:contains:objectType must be a non-empty string" 358 | isError: true 359 | performance: 360 | maxResponseTime: "800ms" 361 | stderr: "toBeEmpty" 362 | 363 | - it: "should reject empty objectType parameter with validation error" 364 | request: 365 | jsonrpc: "2.0" 366 | id: "empty-object-type" 367 | method: "tools/call" 368 | params: 369 | name: "search_system_object_attribute_groups" 370 | arguments: 371 | objectType: "" 372 | searchRequest: 373 | query: 374 | match_all_query: {} 375 | count: 3 376 | expect: 377 | response: 378 | jsonrpc: "2.0" 379 | id: "empty-object-type" 380 | result: 381 | content: 382 | - type: "text" 383 | text: "match:contains:objectType must be a non-empty string" 384 | isError: true 385 | performance: 386 | maxResponseTime: "800ms" 387 | stderr: "toBeEmpty" 388 | 389 | - it: "should handle missing searchRequest parameter by defaulting to match_all_query (mock server behavior)" 390 | request: 391 | jsonrpc: "2.0" 392 | id: "missing-search-request" 393 | method: "tools/call" 394 | params: 395 | name: "search_system_object_attribute_groups" 396 | arguments: 397 | objectType: "Product" 398 | expect: 399 | response: 400 | jsonrpc: "2.0" 401 | id: "missing-search-request" 402 | result: 403 | content: 404 | - type: "text" 405 | text: "match:contains:object_attribute_group_search_result" 406 | isError: false 407 | performance: 408 | maxResponseTime: "2000ms" 409 | stderr: "toBeEmpty" 410 | 411 | - it: "should handle invalid objectType with OCAPI 404 error" 412 | request: 413 | jsonrpc: "2.0" 414 | id: "invalid-object-type" 415 | method: "tools/call" 416 | params: 417 | name: "search_system_object_attribute_groups" 418 | arguments: 419 | objectType: "InvalidObjectType" 420 | searchRequest: 421 | query: 422 | match_all_query: {} 423 | count: 3 424 | expect: 425 | response: 426 | jsonrpc: "2.0" 427 | id: "invalid-object-type" 428 | result: 429 | content: 430 | - type: "text" 431 | text: "match:contains:ObjectTypeNotFoundException" 432 | isError: true 433 | performance: 434 | maxResponseTime: "2000ms" 435 | stderr: "toBeEmpty" 436 | 437 | - it: "should include specific error message for invalid object type" 438 | request: 439 | jsonrpc: "2.0" 440 | id: "invalid-object-type-message" 441 | method: "tools/call" 442 | params: 443 | name: "search_system_object_attribute_groups" 444 | arguments: 445 | objectType: "NonExistentObject" 446 | searchRequest: 447 | query: 448 | match_all_query: {} 449 | count: 2 450 | expect: 451 | response: 452 | jsonrpc: "2.0" 453 | id: "invalid-object-type-message" 454 | result: 455 | content: 456 | - type: "text" 457 | text: "match:regex:[\\s\\S]*No object type with ID[\\s\\S]*NonExistentObject[\\s\\S]*could be found[\\s\\S]*" 458 | isError: true 459 | stderr: "toBeEmpty" 460 | 461 | # ================================================================================== 462 | # QUERY TYPE VALIDATION 463 | # ================================================================================== 464 | - it: "should support bool_query with must conditions" 465 | request: 466 | jsonrpc: "2.0" 467 | id: "bool-query-must" 468 | method: "tools/call" 469 | params: 470 | name: "search_system_object_attribute_groups" 471 | arguments: 472 | objectType: "Product" 473 | searchRequest: 474 | query: 475 | bool_query: 476 | must: 477 | - text_query: 478 | fields: ["id"] 479 | search_phrase: "Channel" 480 | count: 3 481 | expect: 482 | response: 483 | jsonrpc: "2.0" 484 | id: "bool-query-must" 485 | result: 486 | content: 487 | - type: "text" 488 | text: "match:contains:object_attribute_group_search_result" 489 | isError: false 490 | performance: 491 | maxResponseTime: "2000ms" 492 | stderr: "toBeEmpty" 493 | 494 | - it: "should support term_query with exact matching" 495 | request: 496 | jsonrpc: "2.0" 497 | id: "term-query-exact" 498 | method: "tools/call" 499 | params: 500 | name: "search_system_object_attribute_groups" 501 | arguments: 502 | objectType: "Product" 503 | searchRequest: 504 | query: 505 | term_query: 506 | fields: ["id"] 507 | operator: "is" 508 | values: ["Order"] 509 | count: 3 510 | expect: 511 | response: 512 | jsonrpc: "2.0" 513 | id: "term-query-exact" 514 | result: 515 | content: 516 | - type: "text" 517 | text: "match:contains:object_attribute_group_search_result" 518 | isError: false 519 | performance: 520 | maxResponseTime: "2000ms" 521 | stderr: "toBeEmpty" 522 | 523 | # ================================================================================== 524 | # PERFORMANCE VALIDATION 525 | # ================================================================================== 526 | - it: "should complete match_all_query search within reasonable time" 527 | request: 528 | jsonrpc: "2.0" 529 | id: "performance-match-all" 530 | method: "tools/call" 531 | params: 532 | name: "search_system_object_attribute_groups" 533 | arguments: 534 | objectType: "Product" 535 | searchRequest: 536 | query: 537 | match_all_query: {} 538 | count: 10 539 | expect: 540 | response: 541 | jsonrpc: "2.0" 542 | id: "performance-match-all" 543 | result: 544 | content: 545 | - type: "text" 546 | text: "match:contains:object_attribute_group_search_result" 547 | isError: false 548 | performance: 549 | maxResponseTime: "1500ms" 550 | stderr: "toBeEmpty" 551 | 552 | - it: "should complete text search within reasonable time" 553 | request: 554 | jsonrpc: "2.0" 555 | id: "performance-text-search" 556 | method: "tools/call" 557 | params: 558 | name: "search_system_object_attribute_groups" 559 | arguments: 560 | objectType: "SitePreferences" 561 | searchRequest: 562 | query: 563 | text_query: 564 | fields: ["id", "display_name"] 565 | search_phrase: "Config" 566 | count: 5 567 | expect: 568 | response: 569 | jsonrpc: "2.0" 570 | id: "performance-text-search" 571 | result: 572 | content: 573 | - type: "text" 574 | text: "match:contains:object_attribute_group_search_result" 575 | isError: false 576 | performance: 577 | maxResponseTime: "2000ms" 578 | stderr: "toBeEmpty" 579 | 580 | # ================================================================================== 581 | # COMPREHENSIVE FEATURE VALIDATION 582 | # ================================================================================== 583 | - it: "should support comprehensive search with all parameters (sorting, pagination, selection)" 584 | request: 585 | jsonrpc: "2.0" 586 | id: "comprehensive-search" 587 | method: "tools/call" 588 | params: 589 | name: "search_system_object_attribute_groups" 590 | arguments: 591 | objectType: "Product" 592 | searchRequest: 593 | query: 594 | match_all_query: {} 595 | sorts: 596 | - field: "id" 597 | sort_order: "desc" 598 | start: 0 599 | count: 4 600 | select: "(**)" 601 | expect: 602 | response: 603 | jsonrpc: "2.0" 604 | id: "comprehensive-search" 605 | result: 606 | content: 607 | - type: "text" 608 | text: "match:regex:[\\s\\S]*object_attribute_group_search_result[\\s\\S]*count.*4[\\s\\S]*" 609 | isError: false 610 | performance: 611 | maxResponseTime: "2000ms" 612 | stderr: "toBeEmpty" ```