This is page 43 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/generate-cartridge-structure.docs-only.programmatic.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Programmatic tests for generate_cartridge_structure tool
3 | *
4 | * These tests provide comprehensive verification of cartridge generation functionality,
5 | * including directory structure validation, file content verification, cleanup operations,
6 | * and edge case handling. All tests use system temp directory for safe isolation.
7 | *
8 | * Response format discovered via aegis query:
9 | * - Success: { content: [{ type: "text", text: "{\"success\": true, \"message\": \"...\", \"createdFiles\": [...], \"createdDirectories\": [...], \"skippedFiles\": []}" }] }
10 | * - Error: { content: [{ type: "text", text: "Error: ..." }], isError: true }
11 | */
12 |
13 | import { test, describe, before, after, beforeEach, afterEach } from 'node:test';
14 | import { strict as assert } from 'node:assert';
15 | import { connect } from 'mcp-aegis';
16 | import { promises as fs } from 'node:fs';
17 | import { join } from 'node:path';
18 | import { tmpdir } from 'node:os';
19 | import { randomBytes } from 'node:crypto';
20 |
21 | describe('generate_cartridge_structure Programmatic Tests', () => {
22 | let client;
23 | let testDirectories = []; // Track directories for cleanup
24 |
25 | before(async () => {
26 | client = await connect('./aegis.config.docs-only.json');
27 | });
28 |
29 | after(async () => {
30 | if (client?.connected) {
31 | await client.disconnect();
32 | }
33 | });
34 |
35 | beforeEach(() => {
36 | // CRITICAL: Clear all buffers to prevent test interference
37 | client.clearAllBuffers(); // Recommended - comprehensive protection
38 | });
39 |
40 | afterEach(async () => {
41 | // Clean up all test directories
42 | for (const dir of testDirectories) {
43 | try {
44 | await fs.rm(dir, { recursive: true, force: true });
45 | } catch (error) {
46 | // Ignore cleanup errors - directory might not exist
47 | console.warn(`Cleanup warning for ${dir}:`, error.message);
48 | }
49 | }
50 | testDirectories = [];
51 | });
52 |
53 | /**
54 | * Create a unique temporary directory for testing
55 | * @returns {string} Absolute path to temporary directory
56 | */
57 | function createTempTestDir() {
58 | const uniqueId = randomBytes(8).toString('hex');
59 | const testDir = join(tmpdir(), `mcp-cartridge-test-${uniqueId}`);
60 | testDirectories.push(testDir);
61 | return testDir;
62 | }
63 |
64 | /**
65 | * Check if a file exists and is readable
66 | * @param {string} filePath - Path to check
67 | * @returns {Promise<boolean>} True if file exists and is readable
68 | */
69 | async function fileExists(filePath) {
70 | try {
71 | await fs.access(filePath, fs.constants.F_OK | fs.constants.R_OK);
72 | return true;
73 | } catch {
74 | return false;
75 | }
76 | }
77 |
78 | /**
79 | * Check if a directory exists and is readable
80 | * @param {string} dirPath - Path to check
81 | * @returns {Promise<boolean>} True if directory exists and is readable
82 | */
83 | async function directoryExists(dirPath) {
84 | try {
85 | const stat = await fs.stat(dirPath);
86 | return stat.isDirectory();
87 | } catch {
88 | return false;
89 | }
90 | }
91 |
92 | /**
93 | * Read and parse JSON response from tool execution
94 | * @param {object} result - MCP tool result
95 | * @returns {object} Parsed response object
96 | */
97 | function parseToolResponse(result) {
98 | assert.equal(result.isError, false, 'Tool should execute successfully');
99 | assert.ok(result.content, 'Result should have content');
100 | assert.equal(result.content.length, 1, 'Should have exactly one content item');
101 | assert.equal(result.content[0].type, 'text', 'Content should be text type');
102 |
103 | const responseText = result.content[0].text;
104 | return JSON.parse(responseText);
105 | }
106 |
107 | describe('Protocol Compliance', () => {
108 | test('should be properly connected to MCP server', async () => {
109 | assert.ok(client.connected, 'Client should be connected');
110 | });
111 |
112 | test('should have generate_cartridge_structure tool available', async () => {
113 | const tools = await client.listTools();
114 | const cartridgeTool = tools.find(tool => tool.name === 'generate_cartridge_structure');
115 |
116 | assert.ok(cartridgeTool, 'generate_cartridge_structure tool should be available');
117 | assert.equal(cartridgeTool.name, 'generate_cartridge_structure');
118 | assert.ok(cartridgeTool.description, 'Tool should have description');
119 | assert.ok(cartridgeTool.inputSchema, 'Tool should have input schema');
120 | assert.equal(cartridgeTool.inputSchema.type, 'object');
121 | assert.ok(cartridgeTool.inputSchema.properties.cartridgeName, 'Tool should have cartridgeName parameter');
122 | assert.ok(cartridgeTool.inputSchema.required.includes('cartridgeName'), 'cartridgeName should be required');
123 | });
124 | });
125 |
126 | describe('Full Project Setup Generation', () => {
127 | test('should create complete project structure with all required files', async () => {
128 | const testDir = createTempTestDir();
129 | const cartridgeName = 'test_full_project';
130 |
131 | const result = await client.callTool('generate_cartridge_structure', {
132 | cartridgeName,
133 | targetPath: testDir,
134 | fullProjectSetup: true
135 | });
136 |
137 | const response = parseToolResponse(result);
138 |
139 | // Verify response structure
140 | assert.equal(response.success, true, 'Operation should be successful');
141 | assert.ok(response.message.includes('Successfully created full project setup'), 'Message should indicate full project setup');
142 | assert.ok(response.message.includes(cartridgeName), 'Message should include cartridge name');
143 | assert.ok(response.message.includes(testDir), 'Message should include target path');
144 | assert.ok(Array.isArray(response.createdFiles), 'Should have createdFiles array');
145 | assert.ok(Array.isArray(response.createdDirectories), 'Should have createdDirectories array');
146 | assert.ok(Array.isArray(response.skippedFiles), 'Should have skippedFiles array');
147 |
148 | // Verify essential project files were created
149 | const expectedProjectFiles = [
150 | 'package.json',
151 | 'dw.json',
152 | 'webpack.config.js',
153 | '.eslintrc.json',
154 | '.stylelintrc.json',
155 | '.eslintignore',
156 | '.gitignore'
157 | ];
158 |
159 | for (const file of expectedProjectFiles) {
160 | const filePath = join(testDir, file);
161 | assert.ok(await fileExists(filePath), `Project file ${file} should exist`);
162 | assert.ok(response.createdFiles.some(f => f.endsWith(file)), `${file} should be in createdFiles list`);
163 | }
164 |
165 | // Verify cartridge-specific files
166 | const cartridgeDir = join(testDir, 'cartridges', cartridgeName);
167 | const expectedCartridgeFiles = [
168 | '.project',
169 | join('cartridge', `${cartridgeName}.properties`)
170 | ];
171 |
172 | for (const file of expectedCartridgeFiles) {
173 | const filePath = join(cartridgeDir, file);
174 | assert.ok(await fileExists(filePath), `Cartridge file ${file} should exist`);
175 | }
176 |
177 | // Verify directory structure
178 | const expectedDirectories = [
179 | testDir,
180 | join(testDir, 'cartridges'),
181 | cartridgeDir,
182 | join(cartridgeDir, 'cartridge'),
183 | join(cartridgeDir, 'cartridge', 'controllers'),
184 | join(cartridgeDir, 'cartridge', 'models'),
185 | join(cartridgeDir, 'cartridge', 'templates'),
186 | join(cartridgeDir, 'cartridge', 'templates', 'default'),
187 | join(cartridgeDir, 'cartridge', 'templates', 'resources'),
188 | join(cartridgeDir, 'cartridge', 'client'),
189 | join(cartridgeDir, 'cartridge', 'client', 'default'),
190 | join(cartridgeDir, 'cartridge', 'client', 'default', 'js'),
191 | join(cartridgeDir, 'cartridge', 'client', 'default', 'scss')
192 | ];
193 |
194 | for (const dir of expectedDirectories) {
195 | assert.ok(await directoryExists(dir), `Directory ${dir} should exist`);
196 | }
197 |
198 | // Verify file contents - check package.json contains cartridge name
199 | const packageJsonPath = join(testDir, 'package.json');
200 | const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
201 | const packageJson = JSON.parse(packageJsonContent);
202 | assert.ok(packageJson.name.includes(cartridgeName), 'package.json should reference cartridge name');
203 | });
204 |
205 | test('should create cartridge with default fullProjectSetup when not specified', async () => {
206 | const testDir = createTempTestDir();
207 | const cartridgeName = 'test_default_full';
208 |
209 | const result = await client.callTool('generate_cartridge_structure', {
210 | cartridgeName,
211 | targetPath: testDir
212 | // fullProjectSetup not specified - should default to true
213 | });
214 |
215 | const response = parseToolResponse(result);
216 |
217 | assert.equal(response.success, true, 'Operation should be successful');
218 | assert.ok(response.message.includes('full project setup'), 'Should create full project setup by default');
219 |
220 | // Verify project files were created (indicates full project setup)
221 | const packageJsonPath = join(testDir, 'package.json');
222 | assert.ok(await fileExists(packageJsonPath), 'package.json should exist with default full project setup');
223 | });
224 | });
225 |
226 | describe('Cartridge-Only Generation', () => {
227 | test('should create cartridge structure without project files when fullProjectSetup is false', async () => {
228 | const testDir = createTempTestDir();
229 | const cartridgeName = 'test_cartridge_only';
230 |
231 | const result = await client.callTool('generate_cartridge_structure', {
232 | cartridgeName,
233 | targetPath: testDir,
234 | fullProjectSetup: false
235 | });
236 |
237 | const response = parseToolResponse(result);
238 |
239 | // Verify response structure
240 | assert.equal(response.success, true, 'Operation should be successful');
241 | assert.ok(response.message.includes('Successfully created cartridge'), 'Message should indicate cartridge creation');
242 | assert.ok(response.message.includes('existing project'), 'Message should indicate existing project context');
243 | assert.ok(response.message.includes(cartridgeName), 'Message should include cartridge name');
244 |
245 | // Verify cartridge files were created
246 | const cartridgeDir = join(testDir, 'cartridges', cartridgeName);
247 | const expectedCartridgeFiles = [
248 | join(cartridgeDir, '.project'),
249 | join(cartridgeDir, 'cartridge', `${cartridgeName}.properties`)
250 | ];
251 |
252 | for (const filePath of expectedCartridgeFiles) {
253 | assert.ok(await fileExists(filePath), `Cartridge file ${filePath} should exist`);
254 | }
255 |
256 | // Verify project files were NOT created
257 | const projectFiles = ['package.json', 'dw.json', 'webpack.config.js'];
258 | for (const file of projectFiles) {
259 | const filePath = join(testDir, file);
260 | assert.ok(!(await fileExists(filePath)), `Project file ${file} should NOT exist in cartridge-only mode`);
261 | }
262 |
263 | // Verify cartridge directory structure exists
264 | const expectedDirs = [
265 | join(testDir, 'cartridges'),
266 | cartridgeDir,
267 | join(cartridgeDir, 'cartridge'),
268 | join(cartridgeDir, 'cartridge', 'controllers'),
269 | join(cartridgeDir, 'cartridge', 'models'),
270 | join(cartridgeDir, 'cartridge', 'templates')
271 | ];
272 |
273 | for (const dir of expectedDirs) {
274 | assert.ok(await directoryExists(dir), `Cartridge directory ${dir} should exist`);
275 | }
276 | });
277 | });
278 |
279 | describe('Target Path Handling', () => {
280 | test('should create cartridge in current working directory when targetPath not specified', async () => {
281 | const cartridgeName = 'test_cwd_cartridge';
282 |
283 | const result = await client.callTool('generate_cartridge_structure', {
284 | cartridgeName
285 | // targetPath not specified - should use current working directory
286 | });
287 |
288 | const response = parseToolResponse(result);
289 |
290 | assert.equal(response.success, true, 'Operation should be successful');
291 | assert.ok(Array.isArray(response.createdFiles), 'Should have createdFiles array');
292 | assert.ok(response.createdFiles.length > 0, 'Should have created files');
293 |
294 | // Clean up files created in current directory
295 | const cwd = process.cwd();
296 | const createdPaths = [...response.createdFiles, ...response.createdDirectories];
297 |
298 | for (const path of createdPaths) {
299 | try {
300 | if (path.startsWith(cwd)) {
301 | const relativePath = path.substring(cwd.length + 1);
302 | if (relativePath.startsWith('cartridges/') || ['package.json', 'dw.json', 'webpack.config.js', '.eslintrc.json', '.stylelintrc.json', '.eslintignore', '.gitignore'].includes(relativePath)) {
303 | await fs.rm(path, { recursive: true, force: true });
304 | }
305 | }
306 | } catch {
307 | // Ignore cleanup errors
308 | }
309 | }
310 | });
311 |
312 | test('should handle absolute target paths correctly', async () => {
313 | const testDir = createTempTestDir();
314 | const cartridgeName = 'test_absolute_path';
315 |
316 | const result = await client.callTool('generate_cartridge_structure', {
317 | cartridgeName,
318 | targetPath: testDir,
319 | fullProjectSetup: false
320 | });
321 |
322 | const response = parseToolResponse(result);
323 |
324 | assert.equal(response.success, true, 'Operation should be successful');
325 |
326 | // Verify files were created in specified absolute path
327 | const cartridgeDir = join(testDir, 'cartridges', cartridgeName);
328 | assert.ok(await directoryExists(cartridgeDir), 'Cartridge should be created in specified absolute path');
329 |
330 | // Verify all created paths are under the specified target directory
331 | for (const createdPath of response.createdFiles) {
332 | assert.ok(createdPath.startsWith(testDir), `Created file ${createdPath} should be under target directory ${testDir}`);
333 | }
334 |
335 | for (const createdDir of response.createdDirectories) {
336 | assert.ok(createdDir.startsWith(testDir) || createdDir === testDir, `Created directory ${createdDir} should be under target directory ${testDir}`);
337 | }
338 | });
339 | });
340 |
341 | describe('Cartridge Name Validation', () => {
342 | test('should reject empty cartridge name', async () => {
343 | const testDir = createTempTestDir();
344 |
345 | const result = await client.callTool('generate_cartridge_structure', {
346 | cartridgeName: '',
347 | targetPath: testDir
348 | });
349 |
350 | assert.equal(result.isError, true, 'Should return error for empty cartridge name');
351 | assert.ok(result.content[0].text.includes('Error'), 'Error message should be present');
352 | assert.ok(result.content[0].text.includes('valid identifier'), 'Should mention valid identifier requirement');
353 | });
354 |
355 | test('should reject missing cartridge name', async () => {
356 | const testDir = createTempTestDir();
357 |
358 | const result = await client.callTool('generate_cartridge_structure', {
359 | targetPath: testDir
360 | // cartridgeName missing
361 | });
362 |
363 | assert.equal(result.isError, true, 'Should return error for missing cartridge name');
364 | assert.ok(result.content[0].text.includes('Error'), 'Error message should be present');
365 | });
366 |
367 | test('should accept valid cartridge names with different formats', async () => {
368 | const testDir = createTempTestDir();
369 | const validNames = [
370 | 'simple_cartridge',
371 | 'plugin-example',
372 | 'my_plugin_123',
373 | 'cart123',
374 | 'a_b_c'
375 | ];
376 |
377 | for (const cartridgeName of validNames) {
378 | const subTestDir = join(testDir, cartridgeName + '_test');
379 | testDirectories.push(subTestDir);
380 |
381 | const result = await client.callTool('generate_cartridge_structure', {
382 | cartridgeName,
383 | targetPath: subTestDir,
384 | fullProjectSetup: false
385 | });
386 |
387 | const response = parseToolResponse(result);
388 | assert.equal(response.success, true, `Cartridge name "${cartridgeName}" should be valid`);
389 | assert.ok(response.message.includes(cartridgeName), `Message should include cartridge name "${cartridgeName}"`);
390 | }
391 | });
392 | });
393 |
394 | describe('File System Integration', () => {
395 | test('should handle directory creation permissions correctly', async () => {
396 | const testDir = createTempTestDir();
397 | const cartridgeName = 'test_permissions';
398 |
399 | const result = await client.callTool('generate_cartridge_structure', {
400 | cartridgeName,
401 | targetPath: testDir,
402 | fullProjectSetup: false
403 | });
404 |
405 | const response = parseToolResponse(result);
406 | assert.equal(response.success, true, 'Should handle directory creation successfully');
407 |
408 | // Verify created directories have appropriate permissions (readable and writable)
409 | const cartridgeDir = join(testDir, 'cartridges', cartridgeName);
410 | const stat = await fs.stat(cartridgeDir);
411 |
412 | // Check that directory is readable and writable by owner
413 | const mode = stat.mode;
414 | const ownerRead = (mode & 0o400) !== 0;
415 | const ownerWrite = (mode & 0o200) !== 0;
416 | const ownerExecute = (mode & 0o100) !== 0;
417 |
418 | assert.ok(ownerRead, 'Directory should be readable by owner');
419 | assert.ok(ownerWrite, 'Directory should be writable by owner');
420 | assert.ok(ownerExecute, 'Directory should be executable by owner');
421 | });
422 |
423 | test('should create files with correct content structure', async () => {
424 | const testDir = createTempTestDir();
425 | const cartridgeName = 'test_file_content';
426 |
427 | const result = await client.callTool('generate_cartridge_structure', {
428 | cartridgeName,
429 | targetPath: testDir,
430 | fullProjectSetup: true
431 | });
432 |
433 | const response = parseToolResponse(result);
434 | assert.equal(response.success, true, 'Should create files successfully');
435 |
436 | // Check package.json structure
437 | const packageJsonPath = join(testDir, 'package.json');
438 | const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
439 | const packageJson = JSON.parse(packageJsonContent);
440 |
441 | assert.ok(packageJson.name, 'package.json should have name field');
442 | assert.ok(packageJson.version, 'package.json should have version field');
443 | assert.ok(packageJson.scripts, 'package.json should have scripts');
444 | assert.ok(packageJson.devDependencies, 'package.json should have devDependencies');
445 |
446 | // Check cartridge properties file
447 | const propertiesPath = join(testDir, 'cartridges', cartridgeName, 'cartridge', `${cartridgeName}.properties`);
448 | const propertiesContent = await fs.readFile(propertiesPath, 'utf-8');
449 |
450 | assert.ok(propertiesContent.includes('demandware.cartridges'), 'Properties file should contain demandware.cartridges');
451 | assert.ok(propertiesContent.includes(cartridgeName), 'Properties file should reference cartridge name');
452 |
453 | // Check .project file (Eclipse project file)
454 | const projectPath = join(testDir, 'cartridges', cartridgeName, '.project');
455 | const projectContent = await fs.readFile(projectPath, 'utf-8');
456 |
457 | assert.ok(projectContent.includes('<name>'), '.project file should contain project name tag');
458 | assert.ok(projectContent.includes(cartridgeName), '.project file should reference cartridge name');
459 | });
460 |
461 | test('should handle nested directory creation correctly', async () => {
462 | const testDir = createTempTestDir();
463 | const cartridgeName = 'test_nested_dirs';
464 |
465 | const result = await client.callTool('generate_cartridge_structure', {
466 | cartridgeName,
467 | targetPath: testDir,
468 | fullProjectSetup: false
469 | });
470 |
471 | const response = parseToolResponse(result);
472 | assert.equal(response.success, true, 'Should create nested directories successfully');
473 |
474 | // Verify deeply nested directories were created
475 | const deepPath = join(testDir, 'cartridges', cartridgeName, 'cartridge', 'client', 'default', 'scss');
476 | assert.ok(await directoryExists(deepPath), 'Deep nested directory structure should be created');
477 |
478 | // Verify specific key directories in the structure exist
479 | const keyDirectories = [
480 | join(testDir, 'cartridges', cartridgeName),
481 | join(testDir, 'cartridges', cartridgeName, 'cartridge'),
482 | join(testDir, 'cartridges', cartridgeName, 'cartridge', 'client'),
483 | join(testDir, 'cartridges', cartridgeName, 'cartridge', 'client', 'default'),
484 | join(testDir, 'cartridges', cartridgeName, 'cartridge', 'client', 'default', 'scss')
485 | ];
486 |
487 | for (const dir of keyDirectories) {
488 | assert.ok(await directoryExists(dir), `Key directory ${dir} should exist`);
489 | }
490 | });
491 | });
492 |
493 | describe('Edge Cases and Error Handling', () => {
494 | test('should handle special characters in target path', async () => {
495 | const specialDir = join(tmpdir(), 'mcp-test-special chars & symbols');
496 | testDirectories.push(specialDir);
497 | const cartridgeName = 'test_special_path';
498 |
499 | const result = await client.callTool('generate_cartridge_structure', {
500 | cartridgeName,
501 | targetPath: specialDir,
502 | fullProjectSetup: false
503 | });
504 |
505 | const response = parseToolResponse(result);
506 | assert.equal(response.success, true, 'Should handle special characters in path');
507 |
508 | const cartridgeDir = join(specialDir, 'cartridges', cartridgeName);
509 | assert.ok(await directoryExists(cartridgeDir), 'Should create cartridge in path with special characters');
510 | });
511 |
512 | test('should report created files and directories accurately', async () => {
513 | const testDir = createTempTestDir();
514 | const cartridgeName = 'test_file_reporting';
515 |
516 | const result = await client.callTool('generate_cartridge_structure', {
517 | cartridgeName,
518 | targetPath: testDir,
519 | fullProjectSetup: true
520 | });
521 |
522 | const response = parseToolResponse(result);
523 | assert.equal(response.success, true, 'Operation should be successful');
524 |
525 | // Verify all reported files actually exist
526 | for (const filePath of response.createdFiles) {
527 | assert.ok(await fileExists(filePath), `Reported file ${filePath} should actually exist`);
528 | }
529 |
530 | // Verify all reported directories actually exist
531 | for (const dirPath of response.createdDirectories) {
532 | assert.ok(await directoryExists(dirPath), `Reported directory ${dirPath} should actually exist`);
533 | }
534 |
535 | // Verify files list is comprehensive - check for essential files
536 | const essentialFiles = ['package.json', 'dw.json', '.gitignore'];
537 | for (const file of essentialFiles) {
538 | assert.ok(response.createdFiles.some(f => f.endsWith(file)), `Essential file ${file} should be in createdFiles list`);
539 | }
540 |
541 | // Verify no duplicate entries in created files/directories
542 | const uniqueFiles = new Set(response.createdFiles);
543 | const uniqueDirs = new Set(response.createdDirectories);
544 |
545 | assert.equal(response.createdFiles.length, uniqueFiles.size, 'createdFiles should not contain duplicates');
546 | assert.equal(response.createdDirectories.length, uniqueDirs.size, 'createdDirectories should not contain duplicates');
547 | });
548 |
549 | test('should handle very long cartridge names appropriately', async () => {
550 | const testDir = createTempTestDir();
551 | const longCartridgeName = 'very_long_cartridge_name_that_exceeds_normal_limits_but_should_still_work_properly_for_testing_purposes';
552 |
553 | const result = await client.callTool('generate_cartridge_structure', {
554 | cartridgeName: longCartridgeName,
555 | targetPath: testDir,
556 | fullProjectSetup: false
557 | });
558 |
559 | // The tool should either succeed or provide a clear error message about name length
560 | if (result.isError) {
561 | assert.ok(result.content[0].text.includes('Error'), 'Should provide clear error for long names');
562 | } else {
563 | const response = parseToolResponse(result);
564 | assert.equal(response.success, true, 'Should handle long cartridge names if accepted');
565 |
566 | const cartridgeDir = join(testDir, 'cartridges', longCartridgeName);
567 | assert.ok(await directoryExists(cartridgeDir), 'Should create directory with long name');
568 | }
569 | });
570 | });
571 |
572 | describe('Response Format Consistency', () => {
573 | test('should always return consistent JSON response structure', async () => {
574 | const testDir = createTempTestDir();
575 | const cartridgeName = 'test_response_format';
576 |
577 | const result = await client.callTool('generate_cartridge_structure', {
578 | cartridgeName,
579 | targetPath: testDir,
580 | fullProjectSetup: false
581 | });
582 |
583 | const response = parseToolResponse(result);
584 |
585 | // Verify required response fields
586 | assert.ok(typeof response.success === 'boolean', 'Response should have boolean success field');
587 | assert.ok(typeof response.message === 'string', 'Response should have string message field');
588 | assert.ok(Array.isArray(response.createdFiles), 'Response should have createdFiles array');
589 | assert.ok(Array.isArray(response.createdDirectories), 'Response should have createdDirectories array');
590 | assert.ok(Array.isArray(response.skippedFiles), 'Response should have skippedFiles array');
591 |
592 | // Verify field content types
593 | for (const file of response.createdFiles) {
594 | assert.ok(typeof file === 'string', 'All createdFiles entries should be strings');
595 | assert.ok(file.length > 0, 'All createdFiles entries should be non-empty');
596 | }
597 |
598 | for (const dir of response.createdDirectories) {
599 | assert.ok(typeof dir === 'string', 'All createdDirectories entries should be strings');
600 | assert.ok(dir.length > 0, 'All createdDirectories entries should be non-empty');
601 | }
602 |
603 | for (const skipped of response.skippedFiles) {
604 | assert.ok(typeof skipped === 'string', 'All skippedFiles entries should be strings');
605 | }
606 | });
607 |
608 | test('should provide meaningful success messages', async () => {
609 | const testDir = createTempTestDir();
610 | const cartridgeName = 'test_success_message';
611 |
612 | // Test full project setup message
613 | const fullResult = await client.callTool('generate_cartridge_structure', {
614 | cartridgeName: cartridgeName + '_full',
615 | targetPath: testDir + '_full',
616 | fullProjectSetup: true
617 | });
618 |
619 | const fullResponse = parseToolResponse(fullResult);
620 | assert.ok(fullResponse.message.includes('full project setup'), 'Full project message should mention full project setup');
621 | assert.ok(fullResponse.message.includes(cartridgeName + '_full'), 'Message should include cartridge name');
622 |
623 | // Test cartridge-only message
624 | const cartridgeResult = await client.callTool('generate_cartridge_structure', {
625 | cartridgeName: cartridgeName + '_only',
626 | targetPath: testDir + '_only',
627 | fullProjectSetup: false
628 | });
629 |
630 | const cartridgeResponse = parseToolResponse(cartridgeResult);
631 | assert.ok(cartridgeResponse.message.includes('existing project'), 'Cartridge-only message should mention existing project');
632 | assert.ok(cartridgeResponse.message.includes(cartridgeName + '_only'), 'Message should include cartridge name');
633 |
634 | // Add these additional test directories to cleanup list
635 | testDirectories.push(testDir + '_full', testDir + '_only');
636 | });
637 | });
638 | });
639 |
```
--------------------------------------------------------------------------------
/tests/mcp/node/search-system-object-attribute-definitions.full-mode.programmatic.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { test, describe, before, after, beforeEach } from 'node:test';
2 | import { strict as assert } from 'node:assert';
3 | import { connect } from 'mcp-aegis';
4 |
5 | describe('search_system_object_attribute_definitions - Full Mode Programmatic Tests', () => {
6 | let client;
7 |
8 | before(async () => {
9 | client = await connect('./aegis.config.with-dw.json');
10 | });
11 |
12 | after(async () => {
13 | if (client?.connected) {
14 | await client.disconnect();
15 | }
16 | });
17 |
18 | beforeEach(() => {
19 | // Critical: Clear stderr to prevent test interference
20 | client.clearStderr();
21 | });
22 |
23 | // Helper functions for common validations
24 | function assertValidMCPResponse(result) {
25 | assert.ok(result.content, 'Should have content');
26 | assert.ok(Array.isArray(result.content), 'Content should be array');
27 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean');
28 | }
29 |
30 | function assertValidSearchResponse(result) {
31 | assertValidMCPResponse(result);
32 | assert.equal(result.isError, false, 'Should not be error');
33 |
34 | const responseText = result.content[0].text;
35 | const responseData = JSON.parse(responseText);
36 |
37 | assert.ok(responseData.hits !== undefined, 'Should have hits array');
38 | assert.ok(Array.isArray(responseData.hits), 'Hits should be array');
39 | assert.ok(typeof responseData.total === 'number', 'Should have total count');
40 |
41 | return responseData;
42 | }
43 |
44 | function assertValidAttributeDefinition(attribute) {
45 | assert.ok(typeof attribute.id === 'string', 'Attribute should have id');
46 | assert.ok(typeof attribute._type === 'string', 'Should have _type');
47 | assert.equal(attribute._type, 'object_attribute_definition', 'Should be object_attribute_definition type');
48 | assert.ok(typeof attribute._resource_state === 'string', 'Should have _resource_state');
49 | assert.ok(typeof attribute.link === 'string', 'Should have link');
50 |
51 | // Note: OCAPI search results only return basic info (id, link, _type, _resource_state)
52 | // Detailed fields like display_name, value_type, mandatory are only available
53 | // when fetching individual attributes via the link
54 | }
55 |
56 | describe('Protocol Compliance and Tool Availability', () => {
57 | test('should be connected to MCP server', async () => {
58 | assert.ok(client.connected, 'Client should be connected');
59 | });
60 |
61 | test('should have search_system_object_attribute_definitions tool available', async () => {
62 | const tools = await client.listTools();
63 | const toolNames = tools.map(tool => tool.name);
64 | assert.ok(toolNames.includes('search_system_object_attribute_definitions'),
65 | 'Tool should be available in full mode');
66 | });
67 |
68 | test('should have proper tool schema definition', async () => {
69 | const tools = await client.listTools();
70 | const tool = tools.find(t => t.name === 'search_system_object_attribute_definitions');
71 |
72 | assert.ok(tool, 'Tool should exist');
73 | assert.ok(tool.description, 'Tool should have description');
74 | assert.ok(tool.inputSchema, 'Tool should have input schema');
75 | assert.equal(tool.inputSchema.type, 'object', 'Schema should be object type');
76 |
77 | // Validate required parameters
78 | const required = tool.inputSchema.required || [];
79 | assert.ok(required.includes('objectType'), 'objectType should be required');
80 | assert.ok(required.includes('searchRequest'), 'searchRequest should be required');
81 | });
82 | });
83 |
84 | describe('Dynamic Test Case Generation', () => {
85 | test('should generate and validate test cases for known object types', async () => {
86 | const knownObjectTypes = ['Product', 'Customer', 'Order', 'Category', 'Site'];
87 |
88 | for (const objectType of knownObjectTypes) {
89 | // Test with match_all_query for each object type
90 | const result = await client.callTool('search_system_object_attribute_definitions', {
91 | objectType: objectType,
92 | searchRequest: {
93 | query: { match_all_query: {} },
94 | count: 5
95 | }
96 | });
97 |
98 | const responseData = assertValidSearchResponse(result);
99 |
100 | // Validate each attribute in the response
101 | responseData.hits.forEach(attribute => {
102 | assertValidAttributeDefinition(attribute);
103 |
104 | // Business logic validation - verify attribute IDs are reasonable
105 | assert.ok(attribute.id.length > 0, 'Attribute ID should not be empty');
106 | assert.ok(!/\s/.test(attribute.id), 'Attribute ID should not contain spaces');
107 |
108 | // Note: Detailed validation of Product-specific fields would require
109 | // fetching individual attributes via their links, which is beyond
110 | // the scope of search functionality testing
111 | });
112 |
113 | assert.ok(responseData.total >= 0, `${objectType} should have non-negative attribute count`);
114 | }
115 | });
116 |
117 | test('should validate query type combinations dynamically', async () => {
118 | const queryTypes = [
119 | { match_all_query: {} },
120 | {
121 | text_query: {
122 | fields: ['id', 'display_name', 'description'],
123 | search_phrase: 'name'
124 | }
125 | },
126 | {
127 | term_query: {
128 | fields: ['value_type'],
129 | operator: 'is',
130 | values: ['string']
131 | }
132 | }
133 | ];
134 |
135 | for (const query of queryTypes) {
136 | const result = await client.callTool('search_system_object_attribute_definitions', {
137 | objectType: 'Product',
138 | searchRequest: { query, count: 3 }
139 | });
140 |
141 | const responseData = assertValidSearchResponse(result);
142 |
143 | // Validate that query type affects results appropriately
144 | if (query.text_query && query.text_query.search_phrase === 'name') {
145 | // Should return attributes related to 'name'
146 | // Note: Not asserting as it depends on actual data and fuzzy matching
147 | responseData.hits.some(attr =>
148 | attr.id.toLowerCase().includes('name') ||
149 | Object.values(attr.display_name).some(name =>
150 | name.toLowerCase().includes('name')
151 | )
152 | );
153 | }
154 |
155 | if (query.term_query && query.term_query.values.includes('string')) {
156 | // Note: OCAPI search results don't include value_type in basic response
157 | // This would require fetching individual attributes to validate
158 | // For search testing, we focus on verifying the search request/response structure
159 | assert.ok(responseData.hits.length >= 0, 'Should return non-negative results');
160 | }
161 | }
162 | });
163 | });
164 |
165 | describe('Complex Query Combinations', () => {
166 | test('should handle bool_query with multiple clauses', async () => {
167 | const complexQuery = {
168 | bool_query: {
169 | must: [
170 | {
171 | term_query: {
172 | fields: ['mandatory'],
173 | operator: 'is',
174 | values: ['true']
175 | }
176 | },
177 | {
178 | term_query: {
179 | fields: ['searchable'],
180 | operator: 'is',
181 | values: ['true']
182 | }
183 | }
184 | ],
185 | should: [
186 | {
187 | text_query: {
188 | fields: ['id'],
189 | search_phrase: 'custom'
190 | }
191 | }
192 | ],
193 | must_not: [
194 | {
195 | term_query: {
196 | fields: ['system'],
197 | operator: 'is',
198 | values: ['true']
199 | }
200 | }
201 | ]
202 | }
203 | };
204 |
205 | const result = await client.callTool('search_system_object_attribute_definitions', {
206 | objectType: 'Product',
207 | searchRequest: {
208 | query: complexQuery,
209 | count: 10
210 | }
211 | });
212 |
213 | const responseData = assertValidSearchResponse(result);
214 |
215 | // Validate that results match the complex query criteria
216 | responseData.hits.forEach(attr => {
217 | // Note: OCAPI search results only return basic attribute info (id, link, _type)
218 | // Detailed validation of mandatory, searchable, system flags would require
219 | // fetching individual attributes via their links. For search API testing,
220 | // we focus on validating the search request/response structure and pagination.
221 | assertValidAttributeDefinition(attr);
222 | assert.ok(attr.id.length > 0, 'Attribute should have valid ID');
223 | });
224 | });
225 |
226 | test('should handle nested bool_query structures', async () => {
227 | const nestedQuery = {
228 | bool_query: {
229 | must: [
230 | {
231 | bool_query: {
232 | should: [
233 | {
234 | term_query: {
235 | fields: ['value_type'],
236 | operator: 'one_of',
237 | values: ['string', 'text']
238 | }
239 | },
240 | {
241 | term_query: {
242 | fields: ['value_type'],
243 | operator: 'is',
244 | values: ['enum-of-string']
245 | }
246 | }
247 | ]
248 | }
249 | }
250 | ]
251 | }
252 | };
253 |
254 | const result = await client.callTool('search_system_object_attribute_definitions', {
255 | objectType: 'Customer',
256 | searchRequest: {
257 | query: nestedQuery,
258 | count: 5,
259 | sorts: [
260 | { field: 'id', sort_order: 'asc' }
261 | ]
262 | }
263 | });
264 |
265 | const responseData = assertValidSearchResponse(result);
266 |
267 | // Validate nested query results
268 | responseData.hits.forEach(attr => {
269 | // Note: OCAPI search results don't include value_type in basic response
270 | // For search API testing, we validate the structure and response format
271 | assertValidAttributeDefinition(attr);
272 | assert.ok(attr.id.length > 0, 'Should have valid attribute ID');
273 | });
274 |
275 | // Validate sorting is applied
276 | if (responseData.hits.length > 1) {
277 | for (let i = 1; i < responseData.hits.length; i++) {
278 | assert.ok(responseData.hits[i].id >= responseData.hits[i-1].id,
279 | 'Results should be sorted by id in ascending order');
280 | }
281 | }
282 | });
283 | });
284 |
285 | describe('Pagination and Large Dataset Handling', () => {
286 | test('should handle pagination correctly across multiple requests', async () => {
287 | const pageSize = 5;
288 | const maxPages = 3;
289 | const allResults = [];
290 | let totalCount = 0;
291 |
292 | for (let page = 0; page < maxPages; page++) {
293 | const result = await client.callTool('search_system_object_attribute_definitions', {
294 | objectType: 'Product',
295 | searchRequest: {
296 | query: { match_all_query: {} },
297 | count: pageSize,
298 | start: page * pageSize,
299 | sorts: [{ field: 'id', sort_order: 'asc' }]
300 | }
301 | });
302 |
303 | const responseData = assertValidSearchResponse(result);
304 |
305 | if (page === 0) {
306 | totalCount = responseData.total;
307 | } else {
308 | assert.equal(responseData.total, totalCount,
309 | 'Total count should be consistent across pages');
310 | }
311 |
312 | // Validate page results
313 | assert.ok(responseData.hits.length <= pageSize,
314 | 'Page should not exceed requested size');
315 |
316 | // Check for duplicates across pages
317 | responseData.hits.forEach(attr => {
318 | const isDuplicate = allResults.some(existing => existing.id === attr.id);
319 | assert.equal(isDuplicate, false, `Attribute ${attr.id} should not appear in multiple pages`);
320 | allResults.push(attr);
321 | });
322 |
323 | // Break if we've reached the end
324 | if (responseData.hits.length < pageSize ||
325 | allResults.length >= responseData.total) {
326 | break;
327 | }
328 | }
329 |
330 | // Validate overall pagination consistency
331 | assert.ok(allResults.length > 0, 'Should have retrieved some results');
332 |
333 | // Validate sorting consistency across pages
334 | if (allResults.length > 1) {
335 | for (let i = 1; i < allResults.length; i++) {
336 | assert.ok(allResults[i].id >= allResults[i-1].id,
337 | 'Results should maintain sort order across pages');
338 | }
339 | }
340 | });
341 |
342 | test('should handle large count requests appropriately', async () => {
343 | const largeCount = 200;
344 |
345 | const result = await client.callTool('search_system_object_attribute_definitions', {
346 | objectType: 'Product',
347 | searchRequest: {
348 | query: { match_all_query: {} },
349 | count: largeCount
350 | }
351 | });
352 |
353 | const responseData = assertValidSearchResponse(result);
354 |
355 | // Validate that server handles large requests (may return fewer than requested)
356 | assert.ok(responseData.hits.length <= largeCount,
357 | 'Should not return more than requested');
358 | assert.ok(responseData.hits.length >= 0,
359 | 'Should return non-negative number of results');
360 |
361 | // Validate that all returned results are valid
362 | responseData.hits.forEach((attr, index) => {
363 | try {
364 | assertValidAttributeDefinition(attr);
365 | } catch (error) {
366 | throw new Error(`Invalid attribute at index ${index}: ${error.message}`);
367 | }
368 | });
369 | });
370 | });
371 |
372 | describe('Cross-Field Validation and Business Logic', () => {
373 | test('should validate attribute relationships and constraints', async () => {
374 | const result = await client.callTool('search_system_object_attribute_definitions', {
375 | objectType: 'Product',
376 | searchRequest: {
377 | query: { match_all_query: {} },
378 | count: 50
379 | }
380 | });
381 |
382 | const responseData = assertValidSearchResponse(result);
383 |
384 | // Business logic validations - based on OCAPI search response structure
385 | responseData.hits.forEach(attr => {
386 | assertValidAttributeDefinition(attr);
387 |
388 | // Validate basic structure returned by OCAPI search
389 | assert.ok(attr.id.length > 0, 'Attribute ID should not be empty');
390 | assert.ok(attr.link.includes('/attribute_definitions/'), 'Link should be valid');
391 | assert.ok(attr._resource_state.length > 0, 'Should have resource state');
392 |
393 | // Note: Detailed attribute properties (value_type, mandatory, searchable, system,
394 | // display_name) are not included in search results. They would need to be
395 | // fetched individually via the attribute's link for detailed validation.
396 | });
397 | });
398 |
399 | test('should validate search result consistency across different sort orders', async () => {
400 | const sortFields = ['id']; // Only test 'id' since it's available in OCAPI search results
401 | const sortOrders = ['asc', 'desc'];
402 | const resultSets = new Map();
403 |
404 | for (const field of sortFields) {
405 | for (const order of sortOrders) {
406 | const result = await client.callTool('search_system_object_attribute_definitions', {
407 | objectType: 'Customer',
408 | searchRequest: {
409 | query: { match_all_query: {} },
410 | count: 10,
411 | sorts: [{ field, sort_order: order }]
412 | }
413 | });
414 |
415 | const responseData = assertValidSearchResponse(result);
416 | const key = `${field}_${order}`;
417 | resultSets.set(key, responseData);
418 |
419 | // Validate sort order is applied - OCAPI search results are sorted by ID
420 | if (responseData.hits.length > 1) {
421 | for (let i = 1; i < responseData.hits.length; i++) {
422 | const current = responseData.hits[i];
423 | const previous = responseData.hits[i-1];
424 |
425 | // For OCAPI search results, we can only sort by 'id' reliably
426 | // since other detailed fields are not included in search response
427 | if (field === 'id') {
428 | const currentValue = current.id;
429 | const previousValue = previous.id;
430 |
431 | if (order === 'asc') {
432 | assert.ok(currentValue >= previousValue,
433 | `${field} should be in ascending order: ${previousValue} <= ${currentValue}`);
434 | } else {
435 | assert.ok(currentValue <= previousValue,
436 | `${field} should be in descending order: ${previousValue} >= ${currentValue}`);
437 | }
438 | }
439 | // Note: Other sort fields (display_name, value_type) are not available
440 | // in OCAPI search results, so we skip detailed validation for those
441 | }
442 | }
443 | }
444 | }
445 |
446 | // Validate that different sort orders return same total count
447 | const totalCounts = Array.from(resultSets.values()).map(data => data.total);
448 | const uniqueTotals = [...new Set(totalCounts)];
449 | assert.equal(uniqueTotals.length, 1,
450 | 'All sort variations should return same total count');
451 | });
452 | });
453 |
454 | describe('Error Recovery and Edge Cases', () => {
455 | test('should handle invalid object types gracefully', async () => {
456 | // Test non-existent object types (return empty results)
457 | const invalidObjectTypes = ['InvalidObject', 'NonExistent'];
458 |
459 | for (const objectType of invalidObjectTypes) {
460 | const result = await client.callTool('search_system_object_attribute_definitions', {
461 | objectType: objectType,
462 | searchRequest: {
463 | query: { match_all_query: {} },
464 | count: 5
465 | }
466 | });
467 |
468 | // OCAPI returns successful response with empty results for invalid object types
469 | const responseData = assertValidSearchResponse(result);
470 | assert.equal(responseData.total, 0,
471 | `Should return 0 results for invalid object type: ${objectType}`);
472 | assert.equal(responseData.hits.length, 0,
473 | `Should return empty hits array for invalid object type: ${objectType}`);
474 | }
475 |
476 | // Test empty object type (returns validation error)
477 | const emptyResult = await client.callTool('search_system_object_attribute_definitions', {
478 | objectType: '',
479 | searchRequest: {
480 | query: { match_all_query: {} },
481 | count: 5
482 | }
483 | });
484 |
485 | assert.equal(emptyResult.isError, true, 'Should return error for empty object type');
486 | const errorText = emptyResult.content[0].text;
487 | assert.ok(errorText.includes('objectType must be a non-empty string'),
488 | 'Error message should indicate objectType validation issue');
489 | });
490 |
491 | test('should handle malformed queries gracefully', async () => {
492 | const malformedQueries = [
493 | { invalid_query_type: {} },
494 | { text_query: { missing_required_fields: true } },
495 | { term_query: { fields: [], operator: 'invalid', values: [] } },
496 | { bool_query: { invalid_clause: [] } }
497 | ];
498 |
499 | for (const query of malformedQueries) {
500 | const result = await client.callTool('search_system_object_attribute_definitions', {
501 | objectType: 'Product',
502 | searchRequest: { query, count: 5 }
503 | });
504 |
505 | // Should handle malformed queries gracefully
506 | if (result.isError) {
507 | const errorText = result.content[0].text;
508 | assert.ok(errorText.length > 0, 'Error message should not be empty');
509 | } else {
510 | // If not an error, should return valid response structure
511 | assertValidSearchResponse(result);
512 | }
513 | }
514 | });
515 |
516 | test('should recover from network issues and continue working', async () => {
517 | // Test normal operation
518 | const normalResult = await client.callTool('search_system_object_attribute_definitions', {
519 | objectType: 'Product',
520 | searchRequest: {
521 | query: { match_all_query: {} },
522 | count: 3
523 | }
524 | });
525 |
526 | assertValidSearchResponse(normalResult);
527 |
528 | // Test with missing query field - OCAPI provides default behavior (match_all_query)
529 | const missingQueryResult = await client.callTool('search_system_object_attribute_definitions', {
530 | objectType: 'Product',
531 | searchRequest: {
532 | // Missing query field - OCAPI defaults to match_all_query
533 | count: 3
534 | }
535 | });
536 |
537 | // OCAPI provides graceful defaults rather than errors
538 | const missingQueryData = assertValidSearchResponse(missingQueryResult);
539 | assert.ok(missingQueryData.total >= 0, 'Should return valid results with default query');
540 |
541 | // Test that service continues to work normally after edge case
542 | const recoveryResult = await client.callTool('search_system_object_attribute_definitions', {
543 | objectType: 'Customer',
544 | searchRequest: {
545 | query: { match_all_query: {} },
546 | count: 3
547 | }
548 | });
549 |
550 | assertValidSearchResponse(recoveryResult);
551 | assert.ok(recoveryResult.content[0].text.length > 0,
552 | 'Service should continue working normally');
553 | });
554 | });
555 |
556 | describe('Multi-Step Workflow Validation', () => {
557 | test('should support attribute discovery and detailed analysis workflow', async () => {
558 | // Step 1: Discover all attributes for an object type
559 | const discoveryResult = await client.callTool('search_system_object_attribute_definitions', {
560 | objectType: 'Product',
561 | searchRequest: {
562 | query: { match_all_query: {} },
563 | count: 10,
564 | select: '(**)'
565 | }
566 | });
567 |
568 | const discoveryData = assertValidSearchResponse(discoveryResult);
569 | assert.ok(discoveryData.hits.length > 0, 'Should discover some attributes');
570 |
571 | // Step 2: Analyze specific attribute types found in step 1
572 | const valueTypes = [...new Set(discoveryData.hits.map(attr => attr.value_type))];
573 |
574 | for (const valueType of valueTypes.slice(0, 3)) { // Test first 3 types
575 | const typeAnalysisResult = await client.callTool('search_system_object_attribute_definitions', {
576 | objectType: 'Product',
577 | searchRequest: {
578 | query: {
579 | term_query: {
580 | fields: ['value_type'],
581 | operator: 'is',
582 | values: [valueType]
583 | }
584 | },
585 | count: 5
586 | }
587 | });
588 |
589 | const typeData = assertValidSearchResponse(typeAnalysisResult);
590 |
591 | // Validate that all results have the expected value type
592 | typeData.hits.forEach(attr => {
593 | // Note: OCAPI search results don't include value_type field
594 | // The query filtering happens server-side, so we validate structure instead
595 | assertValidAttributeDefinition(attr);
596 | assert.ok(attr.id.length > 0, `Should have valid attribute ID for type search: ${valueType}`);
597 | });
598 | }
599 |
600 | // Step 3: Analyze attribute relationships (mandatory vs optional)
601 | const mandatoryResult = await client.callTool('search_system_object_attribute_definitions', {
602 | objectType: 'Product',
603 | searchRequest: {
604 | query: {
605 | term_query: {
606 | fields: ['mandatory'],
607 | operator: 'is',
608 | values: ['true']
609 | }
610 | },
611 | count: 10
612 | }
613 | });
614 |
615 | const mandatoryData = assertValidSearchResponse(mandatoryResult);
616 |
617 | mandatoryData.hits.forEach(attr => {
618 | // Note: OCAPI search results don't include mandatory field in basic response
619 | // The query filtering happens server-side, so we validate the response structure
620 | assertValidAttributeDefinition(attr);
621 | assert.ok(attr.id.length > 0, 'Should have valid attribute ID');
622 | });
623 |
624 | // Validate workflow consistency
625 | const totalMandatory = mandatoryData.total;
626 | assert.ok(totalMandatory >= 0, 'Should have non-negative mandatory attribute count');
627 | assert.ok(totalMandatory <= discoveryData.total,
628 | 'Mandatory attributes should be subset of all attributes');
629 | });
630 |
631 | test('should support complex search refinement workflow', async () => {
632 | // Step 1: Broad search
633 | const broadResult = await client.callTool('search_system_object_attribute_definitions', {
634 | objectType: 'Customer',
635 | searchRequest: {
636 | query: {
637 | text_query: {
638 | fields: ['id', 'display_name'],
639 | search_phrase: 'address'
640 | }
641 | },
642 | count: 20
643 | }
644 | });
645 |
646 | const broadData = assertValidSearchResponse(broadResult);
647 |
648 | // Step 2: Refine to only searchable address-related attributes
649 | const refinedResult = await client.callTool('search_system_object_attribute_definitions', {
650 | objectType: 'Customer',
651 | searchRequest: {
652 | query: {
653 | bool_query: {
654 | must: [
655 | {
656 | text_query: {
657 | fields: ['id', 'display_name'],
658 | search_phrase: 'address'
659 | }
660 | },
661 | {
662 | term_query: {
663 | fields: ['searchable'],
664 | operator: 'is',
665 | values: ['true']
666 | }
667 | }
668 | ]
669 | }
670 | },
671 | count: 20
672 | }
673 | });
674 |
675 | const refinedData = assertValidSearchResponse(refinedResult);
676 |
677 | // Validate refinement logic
678 | assert.ok(refinedData.total <= broadData.total,
679 | 'Refined search should return same or fewer results');
680 |
681 | refinedData.hits.forEach(attr => {
682 | // Note: OCAPI search results don't include searchable field in basic response
683 | // The query filtering happens server-side, so we validate response structure
684 | assertValidAttributeDefinition(attr);
685 | assert.ok(attr.id.length > 0, 'Should have valid attribute ID');
686 |
687 | // Should contain address-related terms - validating with expression
688 | // Note: This validation works with attribute IDs which are available
689 | const containsAddressTerm = attr.id.toLowerCase().includes('address');
690 | // Expression evaluated for documentation purposes
691 | assert.ok(typeof containsAddressTerm === 'boolean', 'Should evaluate address term check');
692 | });
693 |
694 | // Step 3: Further refine to only custom (non-system) attributes
695 | const customResult = await client.callTool('search_system_object_attribute_definitions', {
696 | objectType: 'Customer',
697 | searchRequest: {
698 | query: {
699 | bool_query: {
700 | must: [
701 | {
702 | text_query: {
703 | fields: ['id', 'display_name'],
704 | search_phrase: 'address'
705 | }
706 | },
707 | {
708 | term_query: {
709 | fields: ['searchable'],
710 | operator: 'is',
711 | values: ['true']
712 | }
713 | }
714 | ],
715 | must_not: [
716 | {
717 | term_query: {
718 | fields: ['system'],
719 | operator: 'is',
720 | values: ['true']
721 | }
722 | }
723 | ]
724 | }
725 | },
726 | count: 20
727 | }
728 | });
729 |
730 | const customData = assertValidSearchResponse(customResult);
731 |
732 | // Validate final refinement
733 | assert.ok(customData.total <= refinedData.total,
734 | 'Custom search should return same or fewer results than refined search');
735 |
736 | customData.hits.forEach(attr => {
737 | // Note: OCAPI search results don't include searchable/system fields in basic response
738 | // The query filtering happens server-side, so we validate response structure
739 | assertValidAttributeDefinition(attr);
740 | assert.ok(attr.id.length > 0, 'Should have valid attribute ID');
741 | });
742 | });
743 | });
744 | });
```
--------------------------------------------------------------------------------
/tests/mcp/node/get-best-practice-guide.docs-only.programmatic.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Programmatic tests for get_best_practice_guide tool
3 | *
4 | * These tests provide advanced verification capabilities beyond YAML pattern matching,
5 | * including dynamic validation, comprehensive content analysis,
6 | * cross-guide relationship testing, and advanced error categorization for the SFCC
7 | * best practice guide retrieval functionality.
8 | *
9 | * Response format discovered via aegis query:
10 | * - Success: { content: [{ type: "text", text: "{\"title\":\"...\",\"description\":\"...\",\"sections\":[...],\"content\":\"...\"}" }], isError: false }
11 | * - Error: { content: [{ type: "text", text: "Error: guideName must be a non-empty string" }], isError: true }
12 | * - Invalid guide: { content: [{ type: "text", text: "null" }], isError: false }
13 | * - JSON structure: title, description, sections array, content markdown
14 | */
15 |
16 | import { test, describe, before, after, beforeEach } from 'node:test';
17 | import { strict as assert } from 'node:assert';
18 | import { connect } from 'mcp-aegis';
19 |
20 | /**
21 | * Content analysis utility for comprehensive guide validation
22 | */
23 | class ContentAnalyzer {
24 | constructor() {
25 | this.contentPatterns = {
26 | security: [
27 | /CSRF|cross-site request forgery/i,
28 | /XSS|cross-site scripting/i,
29 | /authentication|authorization/i,
30 | /encryption|cryptography/i,
31 | /validation|sanitization/i
32 | ],
33 | performance: [
34 | /performance|optimization/i,
35 | /caching|cache/i,
36 | /memory|cpu/i,
37 | /scalability|throughput/i,
38 | /monitoring|metrics/i
39 | ],
40 | sfra: [
41 | /controller|middleware/i,
42 | /server\.get|server\.post/i,
43 | /SFRA|storefront reference architecture/i,
44 | /isml|template/i,
45 | /scss|sass/i,
46 | /model|view/i
47 | ],
48 | sfra_scss: [
49 | /@import\s+['"]~base\//i,
50 | /\$[a-z0-9_-]+\s*:/i,
51 | /@include|mixins?/i,
52 | /BEM|\b[a-z0-9]+__(?:[a-z0-9-]+)|\b[a-z0-9]+--[a-z0-9-]+/i,
53 | /prefers-reduced-motion|focus ring|wcag/i
54 | ],
55 | cartridge: [
56 | /cartridge path/i,
57 | /override mechanism/i,
58 | /app_storefront_base/i,
59 | /plugin_|app_custom_|int_|bm_/i,
60 | /deployment|upload/i
61 | ]
62 | };
63 | }
64 |
65 | analyzeGuide(guideData) {
66 | const analysis = {
67 | structuralIntegrity: this.checkStructuralIntegrity(guideData),
68 | contentDepth: this.assessContentDepth(guideData),
69 | codeExamples: this.extractCodeExamples(guideData),
70 | crossReferences: this.findCrossReferences(guideData),
71 | technicalAccuracy: this.validateTechnicalContent(guideData),
72 | readabilityMetrics: this.calculateReadabilityMetrics(guideData)
73 | };
74 |
75 | return analysis;
76 | }
77 |
78 | checkStructuralIntegrity(guideData) {
79 | const requiredFields = ['title', 'description', 'sections', 'content'];
80 | const missingFields = requiredFields.filter(field => !guideData[field]);
81 |
82 | return {
83 | isComplete: missingFields.length === 0,
84 | missingFields,
85 | sectionsCount: Array.isArray(guideData.sections) ? guideData.sections.length : 0,
86 | hasValidMarkdown: typeof guideData.content === 'string' && guideData.content.includes('#')
87 | };
88 | }
89 |
90 | assessContentDepth(guideData) {
91 | const content = guideData.content || '';
92 | const wordCount = content.split(/\s+/).length;
93 | const paragraphCount = content.split('\n\n').length;
94 | const headingCount = (content.match(/^#+\s/gm) || []).length;
95 |
96 | return {
97 | wordCount,
98 | paragraphCount,
99 | headingCount,
100 | averageWordsPerParagraph: wordCount / Math.max(paragraphCount, 1),
101 | depthScore: this.calculateDepthScore(wordCount, headingCount, paragraphCount)
102 | };
103 | }
104 |
105 | calculateDepthScore(wordCount, headingCount, paragraphCount) {
106 | // Scoring based on content richness
107 | let score = 0;
108 | if (wordCount > 2000) score += 3;
109 | else if (wordCount > 1000) score += 2;
110 | else if (wordCount > 500) score += 1;
111 |
112 | if (headingCount > 8) score += 2;
113 | else if (headingCount > 4) score += 1;
114 |
115 | if (paragraphCount > 15) score += 2;
116 | else if (paragraphCount > 8) score += 1;
117 |
118 | return Math.min(score, 5); // Max score of 5
119 | }
120 |
121 | extractCodeExamples(guideData) {
122 | const content = guideData.content || '';
123 | const codeBlocks = content.match(/```[\s\S]*?```/g) || [];
124 | const inlineCode = content.match(/`[^`\n]+`/g) || [];
125 |
126 | const languages = codeBlocks.map(block => {
127 | const match = block.match(/```(\w+)/);
128 | return match ? match[1] : 'unknown';
129 | });
130 |
131 | return {
132 | codeBlockCount: codeBlocks.length,
133 | inlineCodeCount: inlineCode.length,
134 | languages: [...new Set(languages)],
135 | hasJavaScript: languages.includes('javascript'),
136 | hasJSON: languages.includes('json'),
137 | hasBash: languages.includes('bash'),
138 | hasHTML: languages.includes('html') || languages.includes('isml')
139 | };
140 | }
141 |
142 | findCrossReferences(guideData) {
143 | const content = guideData.content || '';
144 | const mcpReferences = (content.match(/MCP server|MCP tool|`\w+_\w+`/g) || []).length;
145 | const sfccReferences = (content.match(/dw\.\w+|SFCC|Salesforce Commerce Cloud/g) || []).length;
146 | const externalReferences = (content.match(/https?:\/\/[^\s)]+/g) || []).length;
147 |
148 | return {
149 | mcpReferences,
150 | sfccReferences,
151 | externalReferences,
152 | totalReferences: mcpReferences + sfccReferences + externalReferences,
153 | hasIntegrationWorkflow: content.includes('MCP Integration Workflow')
154 | };
155 | }
156 |
157 | validateTechnicalContent(guideData) {
158 | const content = guideData.content || '';
159 | const guideName = this.inferGuideType(guideData);
160 | const expectedPatterns = this.contentPatterns[guideName] || [];
161 |
162 | const matchedPatterns = expectedPatterns.filter(pattern => pattern.test(content));
163 |
164 | let modernPattern = /dw\.crypto\.Cipher|server\.append|server\.prepend/i;
165 | if (guideName === 'sfra_scss') {
166 | modernPattern = /@include|prefers-reduced-motion|mixins?|@import\s+['"]~base\//i;
167 | }
168 |
169 | return {
170 | guideName,
171 | expectedPatterns: expectedPatterns.length,
172 | matchedPatterns: matchedPatterns.length,
173 | accuracyScore: expectedPatterns.length > 0 ?
174 | (matchedPatterns.length / expectedPatterns.length) : 1,
175 | hasDeprecatedReferences: /WeakCipher|WeakMac|WeakMessageDigest/i.test(content),
176 | hasModernPractices: modernPattern.test(content)
177 | };
178 | }
179 |
180 | inferGuideType(guideData) {
181 | const title = (guideData.title || '').toLowerCase();
182 | if (title.includes('security')) return 'security';
183 | if (title.includes('performance')) return 'performance';
184 | if (title.includes('scss') || title.includes('styling')) return 'sfra_scss';
185 | if (title.includes('sfra') || title.includes('controller')) return 'sfra';
186 | if (title.includes('cartridge')) return 'cartridge';
187 | return 'general';
188 | }
189 |
190 | calculateReadabilityMetrics(guideData) {
191 | const content = guideData.content || '';
192 | const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 0);
193 | const words = content.split(/\s+/).filter(w => w.length > 0);
194 |
195 | const avgWordsPerSentence = words.length / Math.max(sentences.length, 1);
196 | const avgCharsPerWord = words.reduce((sum, word) => sum + word.length, 0) / Math.max(words.length, 1);
197 |
198 | return {
199 | sentenceCount: sentences.length,
200 | wordCount: words.length,
201 | avgWordsPerSentence,
202 | avgCharsPerWord,
203 | readabilityScore: this.calculateFleschScore(avgWordsPerSentence, avgCharsPerWord)
204 | };
205 | }
206 |
207 | calculateFleschScore(avgWordsPerSentence, avgCharsPerWord) {
208 | // Simplified Flesch Reading Ease approximation
209 | return Math.max(0, 206.835 - (1.015 * avgWordsPerSentence) - (84.6 * (avgCharsPerWord / 4.7)));
210 | }
211 | }
212 |
213 | /**
214 | * Error categorization utility for comprehensive error analysis
215 | */
216 | class ErrorAnalyzer {
217 | static categorizeError(errorText) {
218 | const patterns = [
219 | { type: 'validation', keywords: ['required', 'invalid', 'missing', 'empty string'] },
220 | { type: 'not_found', keywords: ['not found', 'does not exist', 'null'] },
221 | { type: 'permission', keywords: ['permission', 'unauthorized', 'forbidden'] },
222 | { type: 'network', keywords: ['connection', 'timeout', 'unreachable'] },
223 | { type: 'parsing', keywords: ['parse', 'syntax', 'format'] }
224 | ];
225 |
226 | for (const pattern of patterns) {
227 | if (pattern.keywords.some(keyword =>
228 | errorText.toLowerCase().includes(keyword))) {
229 | return pattern.type;
230 | }
231 | }
232 | return 'unknown';
233 | }
234 |
235 | static assessErrorQuality(errorText) {
236 | return {
237 | isInformative: errorText.length > 20,
238 | hasContext: errorText.includes('guideName'),
239 | isActionable: /must|should|required|expected/i.test(errorText),
240 | category: this.categorizeError(errorText)
241 | };
242 | }
243 | }
244 |
245 | /**
246 | * Custom assertion helpers for MCP response validation
247 | */
248 | function assertValidMCPResponse(result, expectError = false) {
249 | assert.ok(result.content, 'Should have content');
250 | assert.ok(Array.isArray(result.content), 'Content should be array');
251 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean');
252 | assert.equal(result.isError, expectError, `isError should be ${expectError}`);
253 | }
254 |
255 | function assertGuideContent(result, expectedGuideName) {
256 | assertValidMCPResponse(result);
257 | assert.equal(result.content[0].type, 'text');
258 |
259 | const text = result.content[0].text;
260 | if (text === 'null') {
261 | // Invalid guide name returns null
262 | return null;
263 | }
264 |
265 | const guideData = JSON.parse(text);
266 | assert.ok(guideData.title, 'Guide should have title');
267 | assert.ok(guideData.description, 'Guide should have description');
268 | assert.ok(Array.isArray(guideData.sections), 'Guide should have sections array');
269 | assert.ok(guideData.content, 'Guide should have content');
270 |
271 | if (expectedGuideName) {
272 | assert.ok(
273 | guideData.title.toLowerCase().includes(expectedGuideName.replace('_', ' ')) ||
274 | guideData.description.toLowerCase().includes(expectedGuideName.replace('_', ' ')),
275 | `Guide should be related to ${expectedGuideName}`
276 | );
277 | }
278 |
279 | return guideData;
280 | }
281 |
282 | function assertErrorResponse(result, expectedErrorType) {
283 | assertValidMCPResponse(result, true);
284 | assert.equal(result.content[0].type, 'text');
285 |
286 | const errorText = result.content[0].text;
287 | assert.ok(errorText.includes('Error:'), 'Should be error message');
288 |
289 | const errorAnalysis = ErrorAnalyzer.assessErrorQuality(errorText);
290 | assert.ok(errorAnalysis.isInformative, 'Error should be informative');
291 |
292 | if (expectedErrorType) {
293 | assert.equal(errorAnalysis.category, expectedErrorType,
294 | `Error should be categorized as ${expectedErrorType}`);
295 | }
296 |
297 | return errorAnalysis;
298 | }
299 |
300 | describe('get_best_practice_guide Tool - Advanced Programmatic Tests', () => {
301 | let client;
302 | let contentAnalyzer;
303 |
304 | before(async () => {
305 | client = await connect('./aegis.config.docs-only.json');
306 | contentAnalyzer = new ContentAnalyzer();
307 | });
308 |
309 | after(async () => {
310 | if (client?.connected) {
311 | await client.disconnect();
312 | }
313 | });
314 |
315 | beforeEach(() => {
316 | // CRITICAL: Clear all buffers to prevent leaking between tests
317 | client.clearAllBuffers(); // Recommended - comprehensive protection
318 | });
319 |
320 | describe('Protocol Compliance and Basic Functionality', () => {
321 | test('should retrieve valid cartridge creation guide', async () => {
322 | const result = await client.callTool('get_best_practice_guide', {
323 | guideName: 'cartridge_creation'
324 | });
325 |
326 | const guideData = assertGuideContent(result, 'cartridge');
327 | assert.ok(guideData.title.includes('Cartridge'), 'Should be cartridge guide');
328 | assert.ok(guideData.content.includes('Core Principles'), 'Should have core principles');
329 | });
330 |
331 | test('should retrieve security best practices guide', async () => {
332 | const result = await client.callTool('get_best_practice_guide', {
333 | guideName: 'security'
334 | });
335 |
336 | const guideData = assertGuideContent(result, 'security');
337 | assert.ok(guideData.title.toLowerCase().includes('secure') ||
338 | guideData.title.toLowerCase().includes('security'), 'Should be security guide');
339 | assert.ok(guideData.content.includes('CSRF') || guideData.content.includes('XSS') ||
340 | guideData.content.includes('Security'), 'Should contain security concepts');
341 | });
342 |
343 | test('should retrieve SFRA client-side JavaScript guide', async () => {
344 | const result = await client.callTool('get_best_practice_guide', {
345 | guideName: 'sfra_client_side_js'
346 | });
347 |
348 | const guideData = assertGuideContent(result);
349 | assert.ok(guideData.title.toLowerCase().includes('client-side javascript') ||
350 | guideData.description.toLowerCase().includes('client-side javascript'),
351 | 'Guide should reference client-side JavaScript in title or description');
352 | assert.ok(/ajax|assets\.js|debounce|event delegation/i.test(guideData.content),
353 | 'Guide content should include client-side patterns like AJAX or assets.js');
354 | });
355 |
356 | test('should retrieve SFRA SCSS guide', async () => {
357 | const result = await client.callTool('get_best_practice_guide', {
358 | guideName: 'sfra_scss'
359 | });
360 |
361 | const guideData = assertGuideContent(result);
362 | assert.ok(guideData.title.toLowerCase().includes('scss') ||
363 | guideData.description.toLowerCase().includes('scss'),
364 | 'Guide should reference SCSS in title or description');
365 | assert.ok(/@import|_variables\.scss|mixins|sass/i.test(guideData.content),
366 | 'Guide content should highlight SCSS override patterns and mixins');
367 | });
368 |
369 | test('should handle invalid guide name gracefully', async () => {
370 | const result = await client.callTool('get_best_practice_guide', {
371 | guideName: 'nonexistent_guide'
372 | });
373 |
374 | assertValidMCPResponse(result);
375 | assert.equal(result.content[0].text, 'null', 'Should return null for invalid guide');
376 | });
377 |
378 | test('should validate required parameters', async () => {
379 | const result = await client.callTool('get_best_practice_guide', {});
380 |
381 | assertErrorResponse(result, 'validation');
382 | });
383 |
384 | test('should handle empty guide name', async () => {
385 | const result = await client.callTool('get_best_practice_guide', {
386 | guideName: ''
387 | });
388 |
389 | assertErrorResponse(result, 'validation');
390 | });
391 | });
392 |
393 | describe('Comprehensive Guide Validation', () => {
394 | const availableGuides = [
395 | 'cartridge_creation',
396 | 'security',
397 | 'performance',
398 | 'sfra_controllers',
399 | 'sfra_models',
400 | 'sfra_client_side_js',
401 | 'sfra_scss',
402 | 'ocapi_hooks',
403 | 'scapi_hooks',
404 | 'scapi_custom_endpoint',
405 | 'isml_templates',
406 | 'job_framework',
407 | 'localserviceregistry'
408 | ];
409 |
410 | test('should validate all available guides have proper structure', async () => {
411 | const guideAnalyses = new Map();
412 |
413 | for (const guideName of availableGuides) {
414 | const result = await client.callTool('get_best_practice_guide', {
415 | guideName
416 | });
417 |
418 | const guideData = assertGuideContent(result);
419 | const analysis = contentAnalyzer.analyzeGuide(guideData);
420 | guideAnalyses.set(guideName, analysis);
421 |
422 | // Structural integrity checks
423 | assert.ok(analysis.structuralIntegrity.isComplete,
424 | `${guideName} should have complete structure`);
425 | assert.ok(analysis.structuralIntegrity.sectionsCount > 0,
426 | `${guideName} should have sections`);
427 | assert.ok(analysis.structuralIntegrity.hasValidMarkdown,
428 | `${guideName} should have valid markdown`);
429 |
430 | // Content depth validation
431 | assert.ok(analysis.contentDepth.wordCount > 500,
432 | `${guideName} should have substantial content (>500 words)`);
433 | assert.ok(analysis.contentDepth.depthScore >= 2,
434 | `${guideName} should have good content depth`);
435 | }
436 |
437 | // Analyze overall guide quality
438 | const averageDepthScore = Array.from(guideAnalyses.values())
439 | .reduce((sum, analysis) => sum + analysis.contentDepth.depthScore, 0) /
440 | guideAnalyses.size;
441 |
442 | assert.ok(averageDepthScore >= 3,
443 | `Average guide depth score should be >= 3, got ${averageDepthScore}`);
444 | });
445 |
446 | test('should validate technical accuracy across guide types', async () => {
447 | const techGuides = ['security', 'sfra_controllers', 'sfra_client_side_js', 'sfra_scss', 'cartridge_creation'];
448 |
449 | for (const guideName of techGuides) {
450 | const result = await client.callTool('get_best_practice_guide', {
451 | guideName
452 | });
453 |
454 | const guideData = assertGuideContent(result);
455 | const analysis = contentAnalyzer.analyzeGuide(guideData);
456 |
457 | // Technical accuracy validation
458 | assert.ok(analysis.technicalAccuracy.accuracyScore >= 0.7,
459 | `${guideName} should have high technical accuracy`);
460 |
461 | // Modern practices validation
462 | if (guideName === 'security') {
463 | assert.ok(analysis.technicalAccuracy.hasModernPractices,
464 | 'Security guide should reference modern practices');
465 | assert.ok(!analysis.technicalAccuracy.hasDeprecatedReferences ||
466 | analysis.technicalAccuracy.hasModernPractices,
467 | 'Security guide should prefer modern over deprecated practices');
468 | }
469 |
470 | // Code examples validation
471 | if (['sfra_controllers', 'sfra_client_side_js', 'sfra_scss', 'cartridge_creation'].includes(guideName)) {
472 | assert.ok(analysis.codeExamples.codeBlockCount > 0,
473 | `${guideName} should have code examples`);
474 |
475 | if (guideName === 'sfra_scss') {
476 | const hasScssLanguage = analysis.codeExamples.languages.includes('scss');
477 | const mentionsScssPatterns = /@import|\$[a-zA-Z_-]+|mixins?/i.test(guideData.content);
478 | assert.ok(hasScssLanguage || mentionsScssPatterns,
479 | 'SFRA SCSS guide should showcase SCSS override patterns');
480 | } else {
481 | assert.ok(analysis.codeExamples.hasJavaScript,
482 | `${guideName} should have JavaScript examples`);
483 | }
484 | }
485 | }
486 | });
487 |
488 | test('should validate cross-references and integration patterns', async () => {
489 | const result = await client.callTool('get_best_practice_guide', {
490 | guideName: 'cartridge_creation'
491 | });
492 |
493 | const guideData = assertGuideContent(result);
494 | const analysis = contentAnalyzer.analyzeGuide(guideData);
495 |
496 | // Cross-reference validation
497 | assert.ok(analysis.crossReferences.mcpReferences > 0,
498 | 'Cartridge guide should reference MCP tools');
499 | assert.ok(analysis.crossReferences.hasIntegrationWorkflow,
500 | 'Cartridge guide should have MCP integration workflow');
501 | assert.ok(analysis.crossReferences.sfccReferences > 0,
502 | 'Cartridge guide should reference SFCC concepts');
503 | });
504 | });
505 |
506 | describe('Advanced Error Handling and Edge Cases', () => {
507 | test('should provide detailed error categorization', async () => {
508 | const errorScenarios = [
509 | { params: {}, expectedCategory: 'validation' },
510 | { params: { guideName: '' }, expectedCategory: 'validation' },
511 | { params: { guideName: null }, expectedCategory: 'validation' },
512 | { params: { guideName: 123 }, expectedCategory: 'validation' }
513 | ];
514 |
515 | for (const scenario of errorScenarios) {
516 | const result = await client.callTool('get_best_practice_guide', scenario.params);
517 |
518 | if (result.isError) {
519 | const errorAnalysis = assertErrorResponse(result, scenario.expectedCategory);
520 | assert.ok(errorAnalysis.isActionable,
521 | 'Error message should be actionable');
522 | } else {
523 | // Some invalid params might return null instead of error
524 | assert.equal(result.content[0].text, 'null',
525 | 'Invalid params should return null if not error');
526 | }
527 | }
528 | });
529 |
530 | test('should handle malformed requests gracefully', async () => {
531 | const malformedScenarios = [
532 | { guideName: 'a'.repeat(1000) }, // Very long guide name
533 | { guideName: 'guide\nwith\nnewlines' }, // Guide name with newlines
534 | { guideName: 'guide with spaces' }, // Guide name with spaces
535 | { extraParam: 'unexpected', guideName: 'security' } // Extra parameters
536 | ];
537 |
538 | for (const params of malformedScenarios) {
539 | const result = await client.callTool('get_best_practice_guide', params);
540 |
541 | // Should either return valid response or informative error
542 | if (result.isError) {
543 | assertErrorResponse(result);
544 | } else {
545 | // If it succeeds or returns null, that's also acceptable
546 | assertValidMCPResponse(result);
547 | }
548 | }
549 | });
550 |
551 | test('should maintain consistency across error conditions', async () => {
552 | const errorConditions = [
553 | { guideName: 'invalid1' },
554 | { guideName: 'invalid2' },
555 | { guideName: 'nonexistent' }
556 | ];
557 |
558 | const errorResponses = [];
559 | for (const params of errorConditions) {
560 | const result = await client.callTool('get_best_practice_guide', params);
561 | errorResponses.push(result);
562 | }
563 |
564 | // All invalid guide names should behave consistently
565 | const responseTypes = errorResponses.map(r =>
566 | r.isError ? 'error' : (r.content[0].text === 'null' ? 'null' : 'unexpected')
567 | );
568 |
569 | const uniqueResponseTypes = [...new Set(responseTypes)];
570 | assert.equal(uniqueResponseTypes.length, 1,
571 | 'All invalid guide names should have consistent response type');
572 | });
573 | });
574 |
575 | describe('Multi-Step Integration Workflows', () => {
576 | test('should support guide discovery to detailed retrieval workflow', async () => {
577 | // Step 1: Discover available guides
578 | const availableResult = await client.callTool('get_available_best_practice_guides', {});
579 | assertValidMCPResponse(availableResult);
580 |
581 | const availableGuides = JSON.parse(availableResult.content[0].text);
582 | assert.ok(Array.isArray(availableGuides), 'Should return guides array');
583 | assert.ok(availableGuides.length > 0, 'Should have available guides');
584 |
585 | // Step 2: Retrieve detailed guide for each discovered guide
586 | for (const guide of availableGuides.slice(0, 3)) { // Test first 3 for performance
587 | const detailResult = await client.callTool('get_best_practice_guide', {
588 | guideName: guide.name
589 | });
590 |
591 | const guideData = assertGuideContent(detailResult);
592 |
593 | // Validate consistency between discovery and detailed retrieval
594 | // Note: Descriptions may be slightly different between discovery and detailed view
595 | assert.ok(guideData.description.toLowerCase().includes('best practices') ||
596 | guideData.description.toLowerCase().includes('guide') ||
597 | guide.description.toLowerCase().includes('best practices'),
598 | 'Guide descriptions should be conceptually consistent');
599 | }
600 | });
601 |
602 | test('should support cross-guide relationship analysis', async () => {
603 | // Get related guides
604 | const cartridgeResult = await client.callTool('get_best_practice_guide', {
605 | guideName: 'cartridge_creation'
606 | });
607 | const securityResult = await client.callTool('get_best_practice_guide', {
608 | guideName: 'security'
609 | });
610 | const sfraResult = await client.callTool('get_best_practice_guide', {
611 | guideName: 'sfra_controllers'
612 | });
613 |
614 | const cartridgeGuide = assertGuideContent(cartridgeResult);
615 | const securityGuide = assertGuideContent(securityResult);
616 | const sfraGuide = assertGuideContent(sfraResult);
617 |
618 | // Analyze cross-references between guides
619 | const cartridgeAnalysis = contentAnalyzer.analyzeGuide(cartridgeGuide);
620 | const securityAnalysis = contentAnalyzer.analyzeGuide(securityGuide);
621 | const sfraAnalysis = contentAnalyzer.analyzeGuide(sfraGuide);
622 |
623 | // Validate that guides appropriately reference each other
624 | assert.ok(cartridgeAnalysis.crossReferences.mcpReferences > 0,
625 | 'Cartridge guide should reference other MCP tools');
626 |
627 | // Security guide should have practical implementation focus
628 | assert.ok(securityAnalysis.codeExamples.codeBlockCount > 0,
629 | 'Security guide should have code examples');
630 |
631 | // SFRA guide should reference server concepts
632 | assert.ok(sfraAnalysis.technicalAccuracy.hasModernPractices,
633 | 'SFRA guide should reference modern practices');
634 | });
635 |
636 | test('should validate comprehensive development workflow coverage', async () => {
637 | // Simulate a complete development workflow
638 | const workflowGuides = [
639 | 'cartridge_creation', // Project setup
640 | 'sfra_controllers', // Server-side implementation
641 | 'sfra_client_side_js', // Client-side enhancements
642 | 'sfra_scss', // Styling and theming overrides
643 | 'security', // Security review
644 | 'performance' // Optimization
645 | ];
646 |
647 | const workflowAnalysis = new Map();
648 |
649 | for (const guideName of workflowGuides) {
650 | const result = await client.callTool('get_best_practice_guide', {
651 | guideName
652 | });
653 |
654 | const guideData = assertGuideContent(result);
655 | const analysis = contentAnalyzer.analyzeGuide(guideData);
656 | workflowAnalysis.set(guideName, analysis);
657 | }
658 |
659 | // Validate workflow completeness
660 | const totalCodeExamples = Array.from(workflowAnalysis.values())
661 | .reduce((sum, analysis) => sum + analysis.codeExamples.codeBlockCount, 0);
662 |
663 | assert.ok(totalCodeExamples >= 10,
664 | 'Workflow guides should provide substantial code examples');
665 |
666 | const totalCrossReferences = Array.from(workflowAnalysis.values())
667 | .reduce((sum, analysis) => sum + analysis.crossReferences.totalReferences, 0);
668 |
669 | assert.ok(totalCrossReferences >= 20,
670 | 'Workflow guides should have extensive cross-references');
671 |
672 | // Validate that each workflow stage has appropriate depth
673 | workflowAnalysis.forEach((analysis, guideName) => {
674 | assert.ok(analysis.contentDepth.depthScore >= 3,
675 | `${guideName} should have sufficient depth for workflow stage`);
676 | });
677 | });
678 | });
679 |
680 | describe('Content Quality and Accessibility', () => {
681 | test('should validate readability across all guides', async () => {
682 | const testGuides = ['cartridge_creation', 'security', 'sfra_controllers', 'sfra_client_side_js', 'sfra_scss'];
683 |
684 | for (const guideName of testGuides) {
685 | const result = await client.callTool('get_best_practice_guide', {
686 | guideName
687 | });
688 |
689 | const guideData = assertGuideContent(result);
690 | const analysis = contentAnalyzer.analyzeGuide(guideData);
691 |
692 | // Readability validation
693 | assert.ok(analysis.readabilityMetrics.avgWordsPerSentence < 25,
694 | `${guideName} should have readable sentence length`);
695 | assert.ok(analysis.readabilityMetrics.readabilityScore > 30,
696 | `${guideName} should have acceptable readability score`);
697 |
698 | // Content structure validation
699 | assert.ok(analysis.contentDepth.headingCount >= 5,
700 | `${guideName} should have good content organization`);
701 | assert.ok(analysis.contentDepth.averageWordsPerParagraph < 200,
702 | `${guideName} should have digestible paragraph sizes`);
703 | }
704 | });
705 |
706 | test('should ensure comprehensive coverage of technical topics', async () => {
707 | const technicalGuides = {
708 | 'security': ['CSRF', 'XSS', 'authentication', 'encryption'],
709 | 'performance': ['performance', 'optimization'], // Simplified expectations
710 | 'sfra_controllers': ['server.get', 'middleware', 'ISML'],
711 | 'sfra_client_side_js': ['ajax', 'assets.js', 'debounce', 'validation'],
712 | 'sfra_scss': ['@import', '_variables.scss', 'mixins', 'scss']
713 | };
714 |
715 | for (const [guideName, expectedTopics] of Object.entries(technicalGuides)) {
716 | const result = await client.callTool('get_best_practice_guide', {
717 | guideName
718 | });
719 |
720 | const guideData = assertGuideContent(result);
721 | const content = guideData.content.toLowerCase();
722 |
723 | const coveredTopics = expectedTopics.filter(topic =>
724 | content.includes(topic.toLowerCase())
725 | );
726 |
727 | const coverageRatio = coveredTopics.length / expectedTopics.length;
728 | assert.ok(coverageRatio >= 0.7,
729 | `${guideName} should cover at least 70% of expected topics, covered ${coverageRatio * 100}%`);
730 | }
731 | });
732 | });
733 | });
734 |
```
--------------------------------------------------------------------------------
/.github/instructions/mcp-node-tests.instructions.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | applyTo: "**/*.programmatic.test.js"
3 | ---
4 | # MCP Aegis - Programmatic Testing Guide for AI Agents
5 |
6 | **Target Audience**: AI coding assistants generating JavaScript/TypeScript programmatic t# Debugging and monitoring
7 | const stderr = client.getStderr(); // Get captured stderr
8 | client.clearStderr(); // Clear stderr buffer (REQUIRED in beforeEach!)
9 |
10 | **⚠️ CRITICAL**: Always include `client.clearStderr()` in your `beforeEach()` hook to prevent stderr from one test affecting the next test. This is a common source of test flakiness. files for Model Context Protocol servers.
11 |
12 | ## Overview
13 |
14 | **Programmatic Testing** provides JavaScript/TypeScript API for complex MCP server testing scenarios that require dynamic validation logic, multi-step workflows, and integration with existing test suites. **For basic functional testing, the YAML-based testing approach (see `../yaml/AGENTS.md`) is more than sufficient and recommended.**
15 |
16 | ### When to Use Programmatic vs YAML Testing
17 |
18 | **Use YAML Testing for:**
19 | - ✅ Basic functional validation (tool discovery, parameter validation, response structure)
20 | - ✅ Standard error handling scenarios
21 | - ✅ Simple input/output verification
22 | - ✅ CI/CD pipeline testing (more reliable across environments)
23 | - ✅ Quick test development and maintenance
24 |
25 | **Use Programmatic Testing for:**
26 | - Complex business logic validation requiring code execution
27 | - Multi-step workflows with state management
28 | - Dynamic test case generation based on server configuration
29 | - Integration with existing JavaScript/TypeScript test suites
30 | - Advanced error recovery and resilience testing
31 |
32 | ### 📚 Key Resources
33 | - **[Programmatic Testing Documentation](https://aegis.rhino-inquisitor.com/programmatic-testing.html)** - Complete guide
34 | - **[API Reference](https://aegis.rhino-inquisitor.com/api-reference.html)** - All methods and properties
35 | - **[Examples Directory](../../examples/)** - Real-world programmatic test files
36 | - **[YAML Testing Guide](../yaml/AGENTS.md)** - Recommended for basic testing scenarios
37 |
38 | ## Quick Setup
39 |
40 | ### 1. Installation and Initialization
41 | ```bash
42 | # Install in project
43 | npm install --save-dev mcp-aegis
44 | # OR
45 | npx mcp-aegis init
46 | ```
47 |
48 | ### 2. Configuration File
49 | Always create `aegis.config.json` first:
50 |
51 | ```json
52 | {
53 | "name": "My MCP Server",
54 | "command": "node",
55 | "args": ["./server.js"],
56 | "startupTimeout": 5000,
57 | "env": {
58 | "NODE_ENV": "test"
59 | }
60 | }
61 | ```
62 |
63 | ### 3. Basic Programmatic Test Structure
64 | File naming convention: `*.programmatic.test.js`
65 |
66 | ```javascript
67 | import { test, describe, before, after, beforeEach } from 'node:test';
68 | import { strict as assert } from 'node:assert';
69 | import { connect } from 'mcp-aegis';
70 |
71 | describe('[SERVER_NAME] Programmatic Tests', () => {
72 | let client;
73 |
74 | before(async () => {
75 | client = await connect('./aegis.config.json');
76 | });
77 |
78 | after(async () => {
79 | if (client?.connected) {
80 | await client.disconnect();
81 | }
82 | });
83 |
84 | beforeEach(() => {
85 | // CRITICAL: Clear all buffers to prevent leaking into next tests
86 | client.clearAllBuffers(); // Recommended - comprehensive protection
87 | // OR: client.clearStderr(); // Minimum - stderr only
88 | });
89 |
90 | test('should list available tools', async () => {
91 | const tools = await client.listTools();
92 | assert.ok(Array.isArray(tools), 'Tools should be array');
93 | assert.ok(tools.length > 0, 'Should have at least one tool');
94 | });
95 |
96 | test('should execute tool successfully', async () => {
97 | const result = await client.callTool('[TOOL_NAME]', { param: 'value' });
98 | assert.ok(result.content, 'Should return content');
99 | assert.equal(result.isError, false, 'Should not be error');
100 | });
101 | });
102 | ```
103 |
104 | ## Quick Debugging with Query Command
105 |
106 | Before writing comprehensive programmatic tests, use the `query` command to rapidly test your server:
107 |
108 | ```bash
109 | # List all available tools
110 | aegis query --config aegis.config.json
111 |
112 | # Test specific tool with arguments
113 | aegis query read_file '{"path": "test.txt"}' --config aegis.config.json
114 |
115 | # Get JSON output for inspection
116 | aegis query calculator '{"operation": "add", "a": 5, "b": 3}' --config aegis.config.json --json
117 | ```
118 |
119 | **Benefits for programmatic testing workflow**:
120 | - **Rapid prototyping**: Verify server behavior before writing test code
121 | - **API exploration**: Discover tool signatures and response formats
122 | - **Debug assistance**: Inspect actual responses to design assertions
123 | - **Development speed**: Test changes instantly without rebuilding test suite
124 |
125 | **Integration with programmatic tests**:
126 | ```javascript
127 | // Use query command findings to create targeted tests
128 | test('should handle file reading as discovered via query', async () => {
129 | // Based on: aegis query read_file '{"path": "test.txt"}'
130 | const result = await client.callTool('read_file', { path: 'test.txt' });
131 |
132 | // Query command showed this response structure:
133 | assert.ok(result.content);
134 | assert.equal(result.content[0].type, 'text');
135 | assert.ok(result.content[0].text.includes('expected content'));
136 | });
137 | ```
138 |
139 | ## API Reference
140 |
141 | ### Main Entry Points
142 | ```javascript
143 | import { createClient, connect } from 'mcp-aegis';
144 |
145 | // Option 1: Create client (not connected)
146 | const client = await createClient('./aegis.config.json');
147 | await client.connect();
148 |
149 | // Option 2: Create and auto-connect
150 | const connectedClient = await connect('./aegis.config.json');
151 |
152 | // Option 3: Inline configuration
153 | const client = await connect({
154 | name: 'My Server',
155 | command: 'node',
156 | args: ['./server.js'],
157 | cwd: './server-directory',
158 | startupTimeout: 5000
159 | });
160 | ```
161 |
162 | ### Core Methods
163 | ```javascript
164 | // Server lifecycle
165 | await client.connect(); // Start server + MCP handshake
166 | await client.disconnect(); // Graceful shutdown
167 | const isConnected = client.connected; // Connection status
168 |
169 | // Tool operations
170 | const tools = await client.listTools(); // Get available tools
171 | const result = await client.callTool(name, arguments); // Execute tool
172 | const response = await client.sendMessage(jsonRpcMessage); // Raw JSON-RPC
173 |
174 | // Debugging and monitoring
175 | const stderr = client.getStderr(); // Get captured stderr
176 | client.clearStderr(); // Clear stderr buffer
177 | ```
178 |
179 | ### Error Handling
180 | ```javascript
181 | try {
182 | const result = await client.callTool('nonexistent_tool', {});
183 | } catch (error) {
184 | assert.ok(error.message.includes('Failed to call tool'));
185 | // Error details available in error object
186 | }
187 |
188 | // Check for execution errors (not exceptions)
189 | const result = await client.callTool('tool_name', { invalid: 'param' });
190 | if (result.isError) {
191 | assert.ok(result.content[0].text.includes('error message'));
192 | }
193 | ```
194 |
195 | ## Critical: Preventing Test Interference
196 |
197 | ### Buffer Leaking Prevention
198 | **The most common source of flaky programmatic tests is buffer leaking between tests.** When one test generates output (stderr, partial stdout messages) and doesn't clear it, subsequent tests may see the output from previous tests, causing unexpected failures.
199 |
200 | #### Always Include beforeEach Hook
201 | ```javascript
202 | beforeEach(() => {
203 | // RECOMMENDED: Clear all buffers to prevent any leaking
204 | client.clearAllBuffers();
205 |
206 | // OR minimum: Clear only stderr (less comprehensive)
207 | // client.clearStderr();
208 | });
209 | ```
210 |
211 | #### Buffer Bleeding Sources
212 | - **Stderr buffer**: Error messages and debug output
213 | - **Stdout buffer**: Partial JSON messages from previous requests
214 | - **Ready state**: Server readiness flag not reset
215 | - **Pending reads**: Lingering message handlers
216 |
217 | **Best Practice**: Use `client.clearAllBuffers()` instead of just `clearStderr()` for comprehensive protection.
218 |
219 | #### Common Anti-Patterns to Avoid
220 | ```javascript
221 | // ❌ WRONG - Missing beforeEach entirely
222 | describe('My Tests', () => {
223 | let client;
224 |
225 | before(async () => {
226 | client = await connect('./config.json');
227 | });
228 |
229 | // Missing beforeEach - tests will leak buffers!
230 |
231 | test('first test', async () => {
232 | const result = await client.callTool('tool', {});
233 | // This test might generate stderr or leave stdout buffer data
234 | });
235 |
236 | test('second test', async () => {
237 | // This test might see output from first test!
238 | assert.equal(client.getStderr(), ''); // Will fail if first test had stderr
239 | });
240 | });
241 |
242 | // ✅ CORRECT - Include beforeEach with clearStderr
243 | describe('My Tests', () => {
244 | let client;
245 |
246 | before(async () => {
247 | client = await connect('./config.json');
248 | });
249 |
250 | beforeEach(() => {
251 | client.clearAllBuffers(); // Prevents all buffer leaking between tests
252 | });
253 |
254 | test('first test', async () => {
255 | const result = await client.callTool('tool', {});
256 | // Any stderr is isolated to this test
257 | });
258 |
259 | test('second test', async () => {
260 | // Clean slate - no stderr from previous tests
261 | assert.equal(client.getStderr(), ''); // Will pass
262 | });
263 | });
264 | ```
265 |
266 | #### Debugging Stderr Issues
267 | If you're experiencing flaky test failures related to unexpected stderr content:
268 |
269 | 1. **Add clearStderr() to beforeEach** - Most common fix
270 | 2. **Check test isolation** - Ensure each test starts with clean state
271 | 3. **Debug stderr content** - Log `client.getStderr()` to see what's leaking
272 | 4. **Use afterEach cleanup** - Optional additional cleanup
273 |
274 | ```javascript
275 | beforeEach(() => {
276 | client.clearStderr();
277 | });
278 |
279 | afterEach(() => {
280 | // Optional: Debug what stderr was generated
281 | const stderr = client.getStderr();
282 | if (stderr) {
283 | console.log('Test generated stderr:', stderr);
284 | }
285 | });
286 | ```
287 |
288 | ## Testing Patterns
289 |
290 | ### 1. Tool Discovery and Validation
291 | ```javascript
292 | describe('Tool Discovery', () => {
293 | test('should list all expected tools', async () => {
294 | const tools = await client.listTools();
295 |
296 | assert.equal(tools.length, 4, 'Should have 4 tools');
297 |
298 | const toolNames = tools.map(tool => tool.name);
299 | assert.ok(toolNames.includes('calculator'));
300 | assert.ok(toolNames.includes('text_processor'));
301 | assert.ok(toolNames.includes('data_validator'));
302 | });
303 |
304 | test('should validate tool schemas', async () => {
305 | const tools = await client.listTools();
306 |
307 | tools.forEach(tool => {
308 | assert.ok(tool.name, 'Tool should have name');
309 | assert.ok(tool.description, 'Tool should have description');
310 | assert.ok(tool.inputSchema, 'Tool should have input schema');
311 | assert.equal(tool.inputSchema.type, 'object');
312 | });
313 | });
314 | });
315 | ```
316 |
317 | ### 2. Tool Execution Testing
318 | ```javascript
319 | describe('Calculator Tool', () => {
320 | test('should perform basic arithmetic', async () => {
321 | const result = await client.callTool('calculator', {
322 | operation: 'add',
323 | a: 15,
324 | b: 27
325 | });
326 |
327 | assert.equal(result.isError, false);
328 | assert.equal(result.content.length, 1);
329 | assert.equal(result.content[0].type, 'text');
330 | assert.equal(result.content[0].text, 'Result: 42');
331 | });
332 |
333 | test('should handle division by zero', async () => {
334 | const result = await client.callTool('calculator', {
335 | operation: 'divide',
336 | a: 10,
337 | b: 0
338 | });
339 |
340 | assert.equal(result.isError, true);
341 | assert.ok(result.content[0].text.includes('division by zero'));
342 | });
343 |
344 | test('should validate required parameters', async () => {
345 | const result = await client.callTool('calculator', {
346 | operation: 'add'
347 | // Missing a and b parameters
348 | });
349 |
350 | assert.equal(result.isError, true);
351 | assert.ok(result.content[0].text.includes('required'));
352 | });
353 | });
354 | ```
355 |
356 | ### 3. Multi-Step Workflows
357 | ```javascript
358 | describe('Multi-Step Agent Workflows', () => {
359 | test('should support complex decision chains', async () => {
360 | // Step 1: Search for information
361 | const searchResult = await client.callTool('search_knowledge', {
362 | query: 'customer support best practices'
363 | });
364 | assert.equal(searchResult.isError, false);
365 |
366 | // Step 2: Analyze findings
367 | const analysisResult = await client.callTool('analyze_content', {
368 | content: searchResult.content[0].text,
369 | focus: 'actionable recommendations'
370 | });
371 | assert.equal(analysisResult.isError, false);
372 |
373 | // Step 3: Generate summary based on analysis
374 | const summaryResult = await client.callTool('generate_summary', {
375 | source_data: analysisResult.content[0].text,
376 | format: 'executive_summary'
377 | });
378 | assert.equal(summaryResult.isError, false);
379 | assert.ok(summaryResult.content[0].text.includes('Executive Summary'));
380 | });
381 |
382 | test('should maintain context across tool calls', async () => {
383 | // Initialize conversation context
384 | const initResult = await client.callTool('conversation_manager', {
385 | action: 'initialize',
386 | user_id: 'test_user_123'
387 | });
388 | assert.equal(initResult.isError, false);
389 |
390 | const sessionId = extractSessionId(initResult.content[0].text);
391 |
392 | // Make context-dependent call
393 | const contextResult = await client.callTool('conversation_manager', {
394 | action: 'recall',
395 | user_id: 'test_user_123',
396 | session_id: sessionId
397 | });
398 | assert.equal(contextResult.isError, false);
399 | assert.ok(contextResult.content[0].text.includes('session found'));
400 | });
401 | });
402 | ```
403 |
404 | ### 4. Error Recovery and Resilience
405 | ```javascript
406 | describe('Error Recovery', () => {
407 | test('should handle failures gracefully', async () => {
408 | // Test normal operation
409 | const normalResult = await client.callTool('external_api_call', {
410 | endpoint: 'users',
411 | action: 'list'
412 | });
413 | assert.equal(normalResult.isError, false);
414 |
415 | // Test failure scenario - should not throw
416 | const failureResult = await client.callTool('external_api_call', {
417 | endpoint: 'invalid_endpoint',
418 | action: 'list'
419 | });
420 | assert.equal(failureResult.isError, true);
421 | assert.ok(failureResult.content[0].text.includes('not found'));
422 |
423 | // Test recovery - should work again
424 | const recoveryResult = await client.callTool('external_api_call', {
425 | endpoint: 'users',
426 | action: 'list'
427 | });
428 | assert.equal(recoveryResult.isError, false);
429 | });
430 |
431 | test('should handle server restart scenarios', async () => {
432 | // Verify initial connection
433 | const tools = await client.listTools();
434 | assert.ok(tools.length > 0);
435 |
436 | // Simulate server issue by calling invalid method
437 | try {
438 | await client.sendMessage({
439 | jsonrpc: '2.0',
440 | id: 'test-1',
441 | method: 'invalid_method',
442 | params: {}
443 | });
444 | } catch (error) {
445 | // Expected error
446 | }
447 |
448 | // Verify connection still works
449 | const toolsAfter = await client.listTools();
450 | assert.ok(toolsAfter.length > 0);
451 | });
452 | });
453 | ```
454 |
455 | ### ⚠️ Critical: Avoid Concurrent Requests
456 |
457 | **Never use `Promise.all()` or concurrent requests** with MCP Aegis's programmatic API. MCP communication uses a single stdio process with shared message handlers and buffers. Concurrent requests can cause:
458 |
459 | - **Buffer conflicts**: Multiple requests writing to the same stdout/stderr streams
460 | - **Message handler interference**: JSON-RPC messages getting mixed or corrupted
461 | - **Race conditions**: Responses arriving out of order or getting lost
462 | - **Unpredictable test failures**: Flaky tests that pass/fail randomly
463 |
464 | ```javascript
465 | // ❌ NEVER DO THIS - Causes buffer/message handler conflicts
466 | const promises = tools.map(tool => client.callTool(tool.name, {}));
467 | const results = await Promise.all(promises); // WILL CAUSE ISSUES!
468 |
469 | // ✅ ALWAYS DO THIS - Sequential requests work reliably
470 | const results = [];
471 | for (const tool of tools) {
472 | const result = await client.callTool(tool.name, {});
473 | results.push(result);
474 | }
475 | ```
476 |
477 | ### 5. Sequential Request Testing
478 | ```javascript
479 | describe('Sequential Request Testing', () => {
480 | test('should handle sequential requests correctly', async () => {
481 | const results = [];
482 |
483 | // Execute requests sequentially to avoid buffer/message handler conflicts
484 | for (let i = 0; i < 10; i++) {
485 | const result = await client.callTool('sequential_operation', { id: i });
486 | results.push(result);
487 | }
488 |
489 | results.forEach((result, i) => {
490 | assert.equal(result.isError, false, `Request ${i} should succeed`);
491 | assert.ok(result.content[0].text.includes(`id: ${i}`));
492 | });
493 | });
494 |
495 | test('should handle large payload processing', async () => {
496 | const largeText = 'x'.repeat(50000); // 50KB text
497 |
498 | const result = await client.callTool('text_processor', {
499 | text: largeText,
500 | operation: 'word_count'
501 | });
502 |
503 | assert.equal(result.isError, false);
504 | assert.ok(result.content[0].text.includes('50000'));
505 | });
506 | });
507 | ```
508 |
509 | ### ⚠️ Performance Testing Not Recommended
510 |
511 | **Performance testing is not recommended in programmatic tests** due to CI environment variability. CI environments have:
512 | - Highly variable resource allocation and sharing
513 | - Unpredictable I/O performance and network latency
514 | - JIT compilation delays and garbage collection interference
515 | - Container and process initialization overhead
516 |
517 | **Use dedicated performance testing tools instead:**
518 | - Load testing frameworks (Artillery, k6, Apache JMeter)
519 | - APM tools (New Relic, DataDog, AppDynamics)
520 | - Custom monitoring with controlled environments
521 | - Synthetic monitoring services
522 |
523 | **If you must include timing validation**, use extremely lenient thresholds (5+ seconds) and focus on detecting major regressions rather than precise performance requirements.
524 |
525 | ### 6. Dynamic Validation Logic
526 | ```javascript
527 | describe('Dynamic Validation', () => {
528 | test('should validate business rules dynamically', async () => {
529 | const tools = await client.listTools();
530 |
531 | // Dynamic validation based on actual tools
532 | const calculatorTool = tools.find(t => t.name === 'calculator');
533 | if (calculatorTool) {
534 | const supportedOps = calculatorTool.inputSchema.properties.operation.enum;
535 |
536 | for (const operation of supportedOps) {
537 | const result = await client.callTool('calculator', {
538 | operation,
539 | a: 10,
540 | b: 5
541 | });
542 | assert.equal(result.isError, false, `Operation ${operation} should work`);
543 | }
544 | }
545 | });
546 |
547 | test('should generate test cases from schema', async () => {
548 | const tools = await client.listTools();
549 |
550 | for (const tool of tools) {
551 | const schema = tool.inputSchema;
552 | const testCases = generateTestCases(schema);
553 |
554 | for (const testCase of testCases) {
555 | const result = await client.callTool(tool.name, testCase.input);
556 |
557 | if (testCase.shouldSucceed) {
558 | assert.equal(result.isError, false,
559 | `${tool.name} should succeed with ${JSON.stringify(testCase.input)}`);
560 | } else {
561 | assert.equal(result.isError, true,
562 | `${tool.name} should fail with ${JSON.stringify(testCase.input)}`);
563 | }
564 | }
565 | }
566 | });
567 | });
568 |
569 | function generateTestCases(schema) {
570 | // Generate test cases based on JSON schema
571 | const testCases = [];
572 |
573 | // Valid case with all required properties
574 | const validCase = {};
575 | for (const [prop, propSchema] of Object.entries(schema.properties || {})) {
576 | if (propSchema.type === 'string') {
577 | validCase[prop] = 'test_value';
578 | } else if (propSchema.type === 'number') {
579 | validCase[prop] = 42;
580 | }
581 | }
582 | testCases.push({ input: validCase, shouldSucceed: true });
583 |
584 | // Invalid case missing required properties
585 | if (schema.required?.length > 0) {
586 | testCases.push({ input: {}, shouldSucceed: false });
587 | }
588 |
589 | return testCases;
590 | }
591 | ```
592 |
593 | ## Advanced Testing Scenarios
594 |
595 | ### State Management Testing
596 | ```javascript
597 | describe('State Management', () => {
598 | test('should maintain state across multiple calls', async () => {
599 | // Initialize state
600 | await client.callTool('state_manager', {
601 | action: 'set',
602 | key: 'test_key',
603 | value: 'test_value'
604 | });
605 |
606 | // Verify state persists
607 | const result = await client.callTool('state_manager', {
608 | action: 'get',
609 | key: 'test_key'
610 | });
611 |
612 | assert.equal(result.content[0].text, 'test_value');
613 | });
614 | });
615 | ```
616 |
617 | ### Integration Testing with External Services
618 | ```javascript
619 | describe('External Service Integration', () => {
620 | test('should handle API dependencies', async () => {
621 | // Mock external service responses if needed
622 | const result = await client.callTool('api_client', {
623 | endpoint: 'https://jsonplaceholder.typicode.com/posts/1',
624 | method: 'GET'
625 | });
626 |
627 | assert.equal(result.isError, false);
628 | const responseData = JSON.parse(result.content[0].text);
629 | assert.ok(responseData.id);
630 | assert.ok(responseData.title);
631 | });
632 | });
633 | ```
634 |
635 | ### Custom Assertion Helpers
636 | ```javascript
637 | // Helper functions for common assertions
638 | function assertValidMCPResponse(result) {
639 | assert.ok(result.content, 'Should have content');
640 | assert.ok(Array.isArray(result.content), 'Content should be array');
641 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean');
642 | }
643 |
644 | function assertTextContent(result, expectedSubstring) {
645 | assertValidMCPResponse(result);
646 | assert.equal(result.content[0].type, 'text');
647 | assert.ok(result.content[0].text.includes(expectedSubstring));
648 | }
649 |
650 | function assertToolSchema(tool) {
651 | assert.ok(tool.name, 'Tool should have name');
652 | assert.ok(tool.description, 'Tool should have description');
653 | assert.ok(tool.inputSchema, 'Tool should have schema');
654 | assert.equal(tool.inputSchema.type, 'object');
655 | }
656 |
657 | // Usage
658 | test('should return valid calculation result', async () => {
659 | const result = await client.callTool('calculator', { operation: 'add', a: 2, b: 3 });
660 | assertTextContent(result, 'Result: 5');
661 | });
662 | ```
663 |
664 | ## Framework Integration
665 |
666 | ### Jest Integration
667 | ```javascript
668 | // jest.config.js
669 | module.exports = {
670 | testEnvironment: 'node',
671 | setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
672 | testMatch: ['**/*.programmatic.test.js']
673 | };
674 |
675 | // test/setup.js
676 | global.mcpClient = null;
677 |
678 | beforeAll(async () => {
679 | const { connect } = require('mcp-aegis');
680 | global.mcpClient = await connect('./aegis.config.json');
681 | });
682 |
683 | afterAll(async () => {
684 | if (global.mcpClient) {
685 | await global.mcpClient.disconnect();
686 | }
687 | });
688 | ```
689 |
690 | ### Mocha Integration
691 | ```javascript
692 | // test/helpers/mcp-setup.js
693 | const { connect } = require('mcp-aegis');
694 |
695 | let client;
696 |
697 | exports.mochaHooks = {
698 | beforeAll: async () => {
699 | client = await connect('./aegis.config.json');
700 | },
701 | afterAll: async () => {
702 | if (client) {
703 | await client.disconnect();
704 | }
705 | }
706 | };
707 |
708 | exports.getClient = () => client;
709 | ```
710 |
711 | ### TypeScript Support
712 | ```typescript
713 | import { test, describe, before, after } from 'node:test';
714 | import { strict as assert } from 'node:assert';
715 | import { connect, MCPClient } from 'mcp-aegis';
716 |
717 | describe('TypeScript MCP Tests', () => {
718 | let client: MCPClient;
719 |
720 | before(async () => {
721 | client = await connect('./aegis.config.json');
722 | });
723 |
724 | after(async () => {
725 | await client.disconnect();
726 | });
727 |
728 | test('should have typed responses', async () => {
729 | const tools = await client.listTools();
730 |
731 | // TypeScript provides full type safety
732 | assert.ok(Array.isArray(tools));
733 | tools.forEach(tool => {
734 | assert.ok(typeof tool.name === 'string');
735 | assert.ok(typeof tool.description === 'string');
736 | assert.ok(typeof tool.inputSchema === 'object');
737 | });
738 | });
739 | });
740 | ```
741 |
742 | ## Best Practices for AI Agents
743 |
744 | ### 1. Test Structure Generation
745 | ```javascript
746 | // Template for generating comprehensive test suites
747 | function generateTestSuite(serverConfig, toolList) {
748 | return `
749 | describe('${serverConfig.name} Programmatic Tests', () => {
750 | let client;
751 |
752 | before(async () => {
753 | client = await connect('${serverConfig.configPath}');
754 | });
755 |
756 | after(async () => {
757 | if (client?.connected) {
758 | await client.disconnect();
759 | }
760 | });
761 |
762 | beforeEach(() => {
763 | client.clearStderr();
764 | });
765 |
766 | describe('Protocol Compliance', () => {
767 | test('should complete MCP handshake', async () => {
768 | assert.ok(client.connected, 'Client should be connected');
769 | });
770 |
771 | test('should list available tools', async () => {
772 | const tools = await client.listTools();
773 | assert.ok(Array.isArray(tools));
774 | assert.equal(tools.length, ${toolList.length});
775 | });
776 | });
777 |
778 | ${toolList.map(tool => generateToolTests(tool)).join('\n\n')}
779 | });`;
780 | }
781 |
782 | function generateToolTests(tool) {
783 | return `
784 | describe('${tool.name} Tool', () => {
785 | test('should execute successfully with valid parameters', async () => {
786 | const result = await client.callTool('${tool.name}', ${JSON.stringify(tool.validParams)});
787 | assert.equal(result.isError, false);
788 | assertValidMCPResponse(result);
789 | });
790 |
791 | test('should handle invalid parameters gracefully', async () => {
792 | const result = await client.callTool('${tool.name}', {});
793 | assert.equal(result.isError, true);
794 | assertTextContent(result, 'required');
795 | });
796 | });`;
797 | }
798 | ```
799 |
800 | ### 2. Configuration Management
801 | ```javascript
802 | // Detect server configuration from project structure
803 | function detectServerConfig(projectPath) {
804 | const packageJson = readPackageJson(projectPath);
805 |
806 | return {
807 | name: packageJson.name || 'MCP Server',
808 | command: detectRuntime(packageJson.engines),
809 | args: [detectServerFile(projectPath)],
810 | cwd: projectPath,
811 | startupTimeout: 5000,
812 | env: {
813 | NODE_ENV: 'test',
814 | ...(packageJson.mcpConfig?.testEnv || {})
815 | }
816 | };
817 | }
818 |
819 | function detectRuntime(engines = {}) {
820 | if (engines.node) return 'node';
821 | if (engines.python) return 'python';
822 | return 'node'; // default
823 | }
824 | ```
825 |
826 | ### 3. Error Pattern Recognition
827 | ```javascript
828 | // Common error patterns to test for
829 | const errorPatterns = [
830 | { type: 'validation', keywords: ['required', 'invalid', 'missing'] },
831 | { type: 'not_found', keywords: ['not found', 'does not exist'] },
832 | { type: 'permission', keywords: ['permission', 'unauthorized', 'forbidden'] },
833 | { type: 'network', keywords: ['connection', 'timeout', 'unreachable'] }
834 | ];
835 |
836 | function categorizeError(errorText) {
837 | for (const pattern of errorPatterns) {
838 | if (pattern.keywords.some(keyword =>
839 | errorText.toLowerCase().includes(keyword))) {
840 | return pattern.type;
841 | }
842 | }
843 | return 'unknown';
844 | }
845 | ```
846 |
847 | ### 4. Built-in Functional Monitoring
848 | ```javascript
849 | // Focus on functional correctness rather than timing
850 | class FunctionalMonitor {
851 | constructor() {
852 | this.results = new Map();
853 | }
854 |
855 | async validateTool(client, toolName, params) {
856 | const result = await client.callTool(toolName, params);
857 |
858 | const validation = {
859 | success: !result.isError,
860 | hasContent: result.content && result.content.length > 0,
861 | contentType: result.content?.[0]?.type,
862 | timestamp: new Date().toISOString()
863 | };
864 |
865 | if (!this.results.has(toolName)) {
866 | this.results.set(toolName, []);
867 | }
868 | this.results.get(toolName).push(validation);
869 |
870 | return { result, validation };
871 | }
872 |
873 | getReliabilityStats(toolName) {
874 | const validations = this.results.get(toolName) || [];
875 | const successful = validations.filter(v => v.success).length;
876 |
877 | return {
878 | totalCalls: validations.length,
879 | successRate: successful / validations.length,
880 | failureRate: (validations.length - successful) / validations.length,
881 | lastValidation: validations[validations.length - 1]?.timestamp
882 | };
883 | }
884 | }
885 | ```
886 |
887 | ## Test Execution Commands
888 |
889 | **CRITICAL**: Use the correct test commands for this SFCC Dev MCP project:
890 |
891 | ```bash
892 | # Run individual MCP programmatic test (CORRECT for this project)
893 | node --test tests/mcp/node/specific-test.programmatic.test.js
894 |
895 | # Run all MCP programmatic tests (CORRECT for this project)
896 | npm run test:mcp:node
897 |
898 | # Run all MCP tests (YAML + programmatic) (CORRECT for this project)
899 | npm run test:mcp:all
900 |
901 | # ❌ WRONG: Don't use npm test with individual files for MCP tests
902 | # npm test -- tests/mcp/node/specific-test.programmatic.test.js # This runs Jest!
903 |
904 | # ❌ WRONG: Don't use Jest for MCP programmatic tests
905 | # npx jest --testMatch="**/*.programmatic.test.js" # Jest doesn't handle MCP tests
906 |
907 | # Watch mode for development (Node.js test runner)
908 | node --test --watch tests/mcp/node/*.programmatic.test.js
909 |
910 | # Jest is used for unit tests only (src/ directory)
911 | jest base-handler.test.ts
912 |
913 | # Complete test suite (Jest + MCP tests)
914 | npm test
915 | ```
916 |
917 | **Package.json Script Reference for this project:**
918 | - `npm run test:mcp:node` → `node --test tests/mcp/node/*.programmatic.test.js`
919 | - `npm run test:mcp:yaml` → Aegis YAML tests (docs-only mode)
920 | - `npm run test:mcp:yaml:full` → Aegis YAML tests (full mode)
921 | - `npm run test:mcp:all` → All MCP tests (YAML + programmatic)
922 | - `npm test` → Jest unit tests + MCP tests
923 |
924 | ---
925 |
926 | **Key Success Factors for Programmatic Testing:**
927 | 1. Always use proper lifecycle management (before/after hooks)
928 | 2. Clear stderr between tests for isolation
929 | 3. Use appropriate assertion libraries and helpers
930 | 4. Include comprehensive error handling tests
931 | 5. Test both success and failure scenarios
932 | 6. Implement performance monitoring for critical operations
933 | 7. Use TypeScript for better development experience
934 | 8. Integrate with existing testing frameworks and CI/CD pipelines
935 |
```
--------------------------------------------------------------------------------
/docs-site/pages/DevelopmentPage.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import React from 'react';
2 | import SEO from '../components/SEO';
3 | import BreadcrumbSchema from '../components/BreadcrumbSchema';
4 | import StructuredData from '../components/StructuredData';
5 | import CodeBlock, { InlineCode } from '../components/CodeBlock';
6 | import { H1, PageSubtitle, H2, H3 } from '../components/Typography';
7 | import { SITE_DATES } from '../constants';
8 |
9 | const DevelopmentPage: React.FC = () => {
10 | const developmentStructuredData = {
11 | "@context": "https://schema.org",
12 | "@type": "TechArticle",
13 | "headline": "Development Guide - SFCC Development MCP Server",
14 | "description": "Contributing to the SFCC Development MCP Server project. Learn the architecture, setup development environment, and contribute new features.",
15 | "author": {
16 | "@type": "Person",
17 | "name": "Thomas Theunen"
18 | },
19 | "publisher": {
20 | "@type": "Person",
21 | "name": "Thomas Theunen"
22 | },
23 | "datePublished": SITE_DATES.PUBLISHED,
24 | "dateModified": SITE_DATES.MODIFIED,
25 | "url": "https://sfcc-mcp-dev.rhino-inquisitor.com/development/",
26 | "about": [
27 | {
28 | "@type": "SoftwareApplication",
29 | "name": "SFCC Development MCP Server",
30 | "applicationCategory": "DeveloperApplication",
31 | "operatingSystem": "Node.js",
32 | "offers": {
33 | "@type": "Offer",
34 | "price": "0",
35 | "priceCurrency": "USD",
36 | "availability": "https://schema.org/InStock"
37 | }
38 | }
39 | ],
40 | "mainEntity": {
41 | "@type": "Guide",
42 | "name": "SFCC MCP Development Guide"
43 | }
44 | };
45 |
46 | return (
47 | <div className="max-w-6xl mx-auto px-6 py-12">
48 | <SEO
49 | title="Development Guide"
50 | description="Contributing to the SFCC Development MCP Server project. Learn the architecture, setup development environment, and contribute new features."
51 | keywords="SFCC Development MCP Server, SFCC MCP development, TypeScript MCP server, Model Context Protocol development, SFCC tools development"
52 | canonical="/development/"
53 | ogType="article"
54 | />
55 | <BreadcrumbSchema items={[
56 | { name: "Home", url: "/" },
57 | { name: "Development", url: "/development/" }
58 | ]} />
59 | <StructuredData data={developmentStructuredData} />
60 |
61 | <H1 id="development-guide">👨💻 Development Guide</H1>
62 | <PageSubtitle>Contributing to the SFCC Development MCP Server project</PageSubtitle>
63 |
64 | <H2 id="getting-started">🚀 Getting Started</H2>
65 |
66 | <H3 id="prerequisites">Prerequisites</H3>
67 | <ul className="list-disc pl-6 space-y-1">
68 | <li><strong>Node.js</strong> 18 or higher</li>
69 | <li><strong>npm</strong> 8 or higher</li>
70 | <li><strong>Git</strong> for version control</li>
71 | <li><strong>TypeScript</strong> knowledge recommended</li>
72 | </ul>
73 |
74 | <H3 id="local-development-setup">Local Development Setup</H3>
75 | <CodeBlock language="bash" code={`
76 | # Clone the repository
77 | git clone https://github.com/taurgis/sfcc-dev-mcp.git
78 | cd sfcc-dev-mcp
79 |
80 | # Install dependencies
81 | npm install
82 |
83 | # Build TypeScript
84 | npm run build
85 |
86 | # Run tests
87 | npm test
88 |
89 | # Start in development mode
90 | npm run dev -- --dw-json /Users/username/sfcc-project/dw.json
91 | `} />
92 |
93 | <H2 id="project-architecture">🏗️ Project Architecture</H2>
94 |
95 | <p className="text-[11px] text-gray-500 mb-4">Surface: <strong>36+ specialized tools</strong> spanning documentation, best practices, SFRA, cartridge generation, runtime logs, job logs, system & custom objects, site preferences, and code versions.</p>
96 | <H3 id="directory-structure">Directory Structure</H3>
97 | <CodeBlock language="text" code={`
98 | sfcc-dev-mcp/
99 | ├── src/
100 | │ ├── main.ts # CLI entry point
101 | │ ├── index.ts # Package exports
102 | │ ├── core/ # Core MCP server & tool definitions
103 | │ │ ├── server.ts # MCP server (registers handlers, capability gating)
104 | │ │ ├── tool-definitions.ts # All tool schemas grouped by category
105 | │ │ └── handlers/ # Modular tool handlers
106 | │ │ ├── base-handler.ts
107 | │ │ ├── docs-handler.ts
108 | │ │ ├── best-practices-handler.ts
109 | │ │ ├── sfra-handler.ts
110 | │ │ ├── log-handler.ts
111 | │ │ ├── job-log-handler.ts
112 | │ │ ├── system-object-handler.ts
113 | │ │ ├── code-version-handler.ts
114 | │ │ └── cartridge-handler.ts
115 | │ ├── clients/ # API & domain clients (logic, not routing)
116 | │ │ ├── base/ # Shared HTTP + auth
117 | │ │ │ ├── http-client.ts
118 | │ │ │ ├── ocapi-auth-client.ts
119 | │ │ │ └── oauth-token.ts
120 | │ │ ├── logs/ # Modular log system (composition)
121 | │ │ │ ├── log-client.ts # Orchestrator
122 | │ │ │ ├── webdav-client-manager.ts # WebDAV auth + setup
123 | │ │ │ ├── log-file-reader.ts # Range / tail reads
124 | │ │ │ ├── log-file-discovery.ts # Listing & filtering
125 | │ │ │ ├── log-processor.ts # Parsing & normalization
126 | │ │ │ ├── log-analyzer.ts # Pattern & health analysis
127 | │ │ │ ├── log-formatter.ts # Output shaping
128 | │ │ │ ├── log-constants.ts # Central constants/config
129 | │ │ │ └── log-types.ts # Type definitions
130 | │ │ ├── docs/ # Modular documentation system
131 | │ │ │ ├── documentation-scanner.ts
132 | │ │ │ ├── class-content-parser.ts
133 | │ │ │ ├── class-name-resolver.ts
134 | │ │ │ ├── referenced-types-extractor.ts
135 | │ │ │ └── index.ts
136 | │ │ ├── docs-client.ts
137 | │ │ ├── sfra-client.ts
138 | │ │ ├── best-practices-client.ts
139 | │ │ ├── cartridge-generation-client.ts
140 | │ │ ├── ocapi/
141 | │ │ │ ├── site-preferences-client.ts
142 | │ │ │ └── system-objects-client.ts
143 | │ │ ├── ocapi-client.ts
144 | │ │ ├── log-client.ts # Backwards compat wrapper
145 | │ └── best-practices-client.ts # (already listed above? keep once)
146 | │ ├── services/ # Dependency injection service layer
147 | │ │ ├── file-system-service.ts
148 | │ │ ├── path-service.ts
149 | │ │ └── index.ts
150 | │ ├── tool-configs/ # Tool grouping & category configs
151 | │ │ ├── docs-tool-config.ts
152 | │ │ ├── sfra-tool-config.ts
153 | │ │ ├── best-practices-tool-config.ts
154 | │ │ ├── log-tool-config.ts
155 | │ │ ├── job-log-tool-config.ts
156 | │ │ ├── system-object-tool-config.ts
157 | │ │ ├── cartridge-tool-config.ts
158 | │ │ └── code-version-tool-config.ts
159 | │ ├── config/
160 | │ │ ├── configuration-factory.ts # Mode & capability resolution
161 | │ │ └── dw-json-loader.ts # Secure dw.json loading
162 | │ ├── utils/
163 | │ │ ├── cache.ts
164 | │ │ ├── logger.ts
165 | │ │ ├── path-resolver.ts
166 | │ │ ├── query-builder.ts
167 | │ │ ├── utils.ts
168 | │ │ ├── validator.ts
169 | │ └── types/
170 | │ └── types.ts
171 | ├── tests/ # Jest + MCP YAML + programmatic tests
172 | │ ├── *.test.ts
173 | │ ├── mcp/yaml/*.mcp.yml # Declarative tool tests
174 | │ ├── mcp/node/*.programmatic.test.js
175 | │ └── servers/webdav/ # Mock WebDAV server fixtures
176 | ├── docs/ # SFCC & best practices markdown sources
177 | ├── docs-site/ # React + Vite documentation site
178 | ├── scripts/ # Conversion & build scripts
179 | └── ai-instructions/ # AI platform instruction sets
180 | `} />
181 |
182 | <H3 id="configuration-system">Configuration & Capability Gating (<InlineCode>src/config/</InlineCode>)</H3>
183 | <ul className="list-disc pl-6 space-y-1">
184 | <li><strong>configuration-factory.ts</strong>: Determines operating mode & derives capabilities (<InlineCode>canAccessLogs</InlineCode>, <InlineCode>canAccessJobLogs</InlineCode>, <InlineCode>canAccessOCAPI</InlineCode>, <InlineCode>canAccessSitePrefs</InlineCode>).</li>
185 | <li><strong>dw-json-loader.ts</strong>: Safe credential ingestion, prevents accidental misuse.</li>
186 | <li><strong>Capability Gating</strong>: No credentials → docs & best practice tools only; WebDAV creds → runtime + job logs; Data API creds → system & custom objects, site preferences, code versions.</li>
187 | <li><strong>Least Privilege</strong>: Tools requiring unavailable capabilities never registered.</li>
188 | </ul>
189 |
190 | <H3 id="adding-new-tools">Adding New Tools (Updated Flow)</H3>
191 | <ol className="list-decimal pl-6 space-y-2">
192 | <li><strong>Define schema</strong> in correct category array inside <InlineCode>tool-definitions.ts</InlineCode>.</li>
193 | <li><strong>Decide placement</strong> – extend existing handler or create new handler extending <InlineCode>BaseToolHandler</InlineCode>.</li>
194 | <li><strong>Implement logic</strong> inside a client/service (keep handler thin).</li>
195 | <li><strong>Register handler</strong> only if new category (update <InlineCode>registerHandlers()</InlineCode> in <InlineCode>server.ts</InlineCode>).</li>
196 | <li><strong>Discover response format</strong>: Run with <InlineCode>npx aegis query [tool]</InlineCode> BEFORE writing tests; capture real JSON shape.</li>
197 | <li><strong>Add tests</strong>: Jest unit + YAML (docs & full mode as applicable) + programmatic Node tests if complex.</li>
198 | <li><strong>Update docs</strong>: This page + README + copilot instructions when categories/counts change.</li>
199 | </ol>
200 |
201 | <H3 id="testing-strategy">Testing Strategy</H3>
202 | <p className="text-xs text-gray-500 mb-2">Programmatic MCP tests must use Node test runner (e.g. <InlineCode>node --test tests/mcp/node/your-test.programmatic.test.js</InlineCode>) — do NOT invoke via <InlineCode>npm test -- file</InlineCode> (Jest only).</p>
203 | <H3 id="unit-tests">Unit Tests</H3>
204 | <CodeBlock language="bash" code={`
205 | # Run all tests
206 | npm test
207 |
208 | # Run specific test file
209 | npm test base-http-client.test.ts
210 |
211 | # Run with coverage
212 | npm run test:coverage
213 |
214 | # Watch mode for development
215 | npm run test:watch
216 | `} />
217 |
218 | <H3 id="available-test-scripts">Available Test Scripts</H3>
219 | <CodeBlock language="bash" code={`
220 | # Core testing scripts from package.json
221 | npm test # Run all tests with Jest
222 | npm run test:watch # Run tests in watch mode
223 | npm run test:coverage # Run tests with coverage report
224 |
225 | # Linting and code quality
226 | npm run lint # Check code style
227 | npm run lint:fix # Auto-fix linting issues
228 | npm run lint:check # Check with zero warnings
229 | `} />
230 |
231 | <H3 id="manual-testing">Manual Testing</H3>
232 | <CodeBlock language="bash" code={`
233 | # Test with real SFCC instance (create your own test-dw.json)
234 | npm run dev -- --dw-json /Users/username/sfcc-project/test-dw.json --debug
235 |
236 | # Test documentation-only mode
237 | npm run dev -- --debug
238 | `} />
239 |
240 | <H2 id="documentation-updates">📚 Documentation Updates</H2>
241 |
242 | <H3 id="updating-sfcc-documentation">Updating SFCC Documentation</H3>
243 |
244 | <p><strong>1. Add/Update Markdown Files</strong> in <InlineCode>docs/</InlineCode>:</p>
245 | <CodeBlock language="bash" code={`
246 | # Add new class documentation
247 | echo "# NewClass\\n\\nDescription..." > docs/dw_catalog/NewClass.md
248 | `} />
249 |
250 | <p><strong>2. Run Documentation Conversion:</strong></p>
251 | <CodeBlock language="bash" code={`
252 | # Convert and process documentation (requires axios and cheerio)
253 | npm run convert-docs
254 |
255 | # Test with limited conversion
256 | npm run convert-docs:test
257 |
258 | # Limited conversion (5 files)
259 | npm run convert-docs:limit
260 | `} />
261 |
262 | <p><strong>3. Test Documentation Tools:</strong></p>
263 | <CodeBlock language="bash" code={`
264 | # Test documentation access with your changes
265 | npm run dev -- --debug true
266 | # Then use MCP client to test get_sfcc_class_info with "NewClass"
267 | `} />
268 |
269 | <H3 id="updating-github-pages">Updating Documentation Site</H3>
270 | <p>The documentation site (<InlineCode>docs-site/</InlineCode>) is a React + Vite app. Deployment is handled by GitHub Actions after changes are pushed to the default branch.</p>
271 | <ol className="list-decimal pl-6 space-y-1">
272 | <li><strong>Edit Content</strong>: Modify or add pages/components under <InlineCode>docs-site/</InlineCode>.</li>
273 | <li><strong>Local Preview</strong>:
274 | <CodeBlock language="bash" code={`cd docs-site
275 | npm install
276 | npm run dev # Opens Vite dev server (default http://localhost:5173)
277 | `} />
278 | </li>
279 | <li><strong>Build (optional check)</strong>:
280 | <CodeBlock language="bash" code={`cd docs-site
281 | npm run build # Generates dist/ with static assets
282 | `} />
283 | </li>
284 | <li><strong>Push Changes</strong>: CI workflow publishes the built site to GitHub Pages.</li>
285 | <li><strong>Search Index / Sitemap</strong>: Automatically generated via build scripts (<InlineCode>generate:search-index</InlineCode>, <InlineCode>generate:sitemap</InlineCode>).</li>
286 | </ol>
287 |
288 | <H2 id="coding-standards">🎯 Coding Standards</H2>
289 |
290 | <H3 id="typescript-guidelines">TypeScript Guidelines</H3>
291 | <CodeBlock language="typescript" code={`
292 | // Use explicit types
293 | interface ToolParams {
294 | readonly query: string;
295 | readonly limit?: number;
296 | }
297 |
298 | // Use proper error handling
299 | async function riskyOperation(): Promise<Result> {
300 | try {
301 | return await performOperation();
302 | } catch (error) {
303 | this.logger.error('Operation failed', { error: error.message });
304 | throw new OperationError('Failed to perform operation', error);
305 | }
306 | }
307 |
308 | // Use meaningful names
309 | const searchProductsByCategory = (categoryId: string) => {
310 | // Implementation
311 | };
312 | `} />
313 |
314 | <H3 id="code-organization">Code Organization</H3>
315 | <ul className="list-disc pl-6 space-y-1">
316 | <li><strong>Single Responsibility</strong>: Each class/function has one clear purpose</li>
317 | <li><strong>Dependency Injection</strong>: Use constructor injection for dependencies</li>
318 | <li><strong>Error Boundaries</strong>: Proper error handling at service boundaries</li>
319 | <li><strong>Logging</strong>: Comprehensive logging for debugging and monitoring</li>
320 | </ul>
321 |
322 | <H3 id="git-workflow">Git Workflow</H3>
323 | <CodeBlock language="bash" code={`
324 | # Create feature branch from develop
325 | git checkout develop
326 | git pull origin develop
327 | git checkout -b feature/new-tool-name
328 |
329 | # Make atomic commits
330 | git add src/clients/my-client.ts
331 | git commit -m "feat: add my new tool implementation"
332 |
333 | git add tests/my-client.test.ts
334 | git commit -m "test: add unit tests for my new tool"
335 |
336 | git add docs-site/tools.md
337 | git commit -m "docs: update tools documentation"
338 |
339 | # Push and create PR to develop branch
340 | git push origin feature/new-tool-name
341 | `} />
342 |
343 | <p><strong>Commit Message Convention:</strong></p>
344 | <ul className="list-disc pl-6 space-y-1">
345 | <li><InlineCode>feat:</InlineCode> - New features</li>
346 | <li><InlineCode>fix:</InlineCode> - Bug fixes</li>
347 | <li><InlineCode>docs:</InlineCode> - Documentation updates</li>
348 | <li><InlineCode>test:</InlineCode> - Test additions/modifications</li>
349 | <li><InlineCode>refactor:</InlineCode> - Code refactoring</li>
350 | <li><InlineCode>chore:</InlineCode> - Build process or auxiliary tool changes</li>
351 | </ul>
352 |
353 | <H2 id="testing-best-practices">🧪 Testing Best Practices</H2>
354 |
355 | <H3 id="test-structure">Test Structure</H3>
356 | <CodeBlock language="typescript" code={`
357 | describe('FeatureName', () => {
358 | let client: MyClient;
359 | let mockHttpClient: jest.Mocked<HttpClient>;
360 |
361 | beforeEach(() => {
362 | mockHttpClient = createMockHttpClient();
363 | client = new MyClient(mockHttpClient, mockLogger);
364 | });
365 |
366 | describe('methodName', () => {
367 | it('should handle success case', async () => {
368 | // Arrange
369 | const input = { query: 'test' };
370 | const mockResponse = { data: 'mock response' };
371 | mockHttpClient.get.mockResolvedValue(mockResponse);
372 |
373 | // Act
374 | const result = await client.methodName(input);
375 |
376 | // Assert
377 | expect(result).toBeDefined();
378 | expect(mockHttpClient.get).toHaveBeenCalledWith('/expected/path');
379 | });
380 |
381 | it('should handle error case', async () => {
382 | // Arrange
383 | const input = { query: 'test' };
384 | mockHttpClient.get.mockRejectedValue(new Error('Network error'));
385 |
386 | // Act & Assert
387 | await expect(client.methodName(input)).rejects.toThrow('Network error');
388 | });
389 | });
390 | });
391 | `} />
392 |
393 | <H3 id="mock-strategy">Mock Strategy</H3>
394 | <CodeBlock language="typescript" code={`
395 | // Mock external dependencies using Jest
396 | const mockLogger = {
397 | info: jest.fn(),
398 | error: jest.fn(),
399 | debug: jest.fn(),
400 | warn: jest.fn()
401 | };
402 |
403 | // Use factories for complex mocks
404 | const createMockSFCCResponse = (overrides = {}) => ({
405 | statusCode: 200,
406 | headers: { 'content-type': 'application/json' },
407 | data: 'Mock response data',
408 | ...overrides
409 | });
410 | `} />
411 |
412 | <H3 id="testing-files-available">Testing Coverage Overview</H3>
413 | <ul className="list-disc pl-6 space-y-1">
414 | <li><strong>Unit Clients</strong>: HTTP/auth, OCAPI subclients, docs, SFRA, best practices, cartridge generation.</li>
415 | <li><strong>Handlers</strong>: Each modular handler has focused tests (error shaping, capability filtering).</li>
416 | <li><strong>Log System</strong>: Discovery, reader, processor, analyzer, formatter modules.</li>
417 | <li><strong>Job Logs</strong>: Parsing & multi-level consolidation logic.</li>
418 | <li><strong>MCP Protocol Tests</strong>: YAML declarative + programmatic (Node) in <InlineCode>tests/mcp/</InlineCode>.</li>
419 | <li><strong>WebDAV Mock</strong>: Integration environment for log + job retrieval.</li>
420 | </ul>
421 |
422 | <H3 id="handler-architecture">Handler Architecture</H3>
423 | <ul className="list-disc pl-6 space-y-1">
424 | <li><strong>BaseToolHandler</strong>: Central timing, error normalization, logger integration.</li>
425 | <li><strong>Category Isolation</strong>: Each functional domain kept small & cohesive.</li>
426 | <li><strong>Extensibility</strong>: New feature area → new handler; minimal churn to existing code.</li>
427 | <li><strong>Testing Benefit</strong>: Handlers test orchestration; clients test domain logic.</li>
428 | </ul>
429 |
430 | <H3 id="services-di">Services & Dependency Injection</H3>
431 | <ul className="list-disc pl-6 space-y-1">
432 | <li><strong>FileSystemService</strong> & <strong>PathService</strong>: Abstract Node APIs for test isolation.</li>
433 | <li><strong>Client Composition</strong>: Pass services or mocks explicitly—no hidden globals.</li>
434 | <li><strong>Deterministic Tests</strong>: Avoids brittle fs/path mocking at module level.</li>
435 | </ul>
436 |
437 | <H3 id="log-architecture">Log & Job Log Architecture</H3>
438 | <ul className="list-disc pl-6 space-y-1">
439 | <li><strong>Reader</strong>: Range tail reads minimize bandwidth.</li>
440 | <li><strong>Processor</strong>: Normalizes raw lines → structured entries.</li>
441 | <li><strong>Analyzer</strong>: Pattern extraction, severity grouping, health scoring.</li>
442 | <li><strong>Formatter</strong>: Produces human-oriented summaries for MCP output.</li>
443 | <li><strong>Job Logs</strong>: Unified multi-level log files consolidated logically.</li>
444 | </ul>
445 |
446 | <H3 id="tool-configs">Tool Config Modules</H3>
447 | <p>Each <InlineCode>tool-configs/*.ts</InlineCode> file groups logically related tool definitions or export sets, enabling cleaner segregation and future dynamic registration strategies.</p>
448 |
449 | <H3 id="caching-performance">Caching & Performance</H3>
450 | <ul className="list-disc pl-6 space-y-1">
451 | <li><strong>cache.ts</strong>: In-memory response caching (documentation & static lookups).</li>
452 | <li><strong>log-cache.ts</strong>: Specialized transient caching for recently tailed segments.</li>
453 | <li><strong>Avoid Premature I/O</strong>: Lazy fetch patterns in log discovery & system objects.</li>
454 | <li><strong>Capability Filter</strong>: Reduces surface area → fewer accidental expensive calls.</li>
455 | </ul>
456 |
457 | <H2 id="release-process">🚀 Release Process</H2>
458 |
459 | <H3 id="version-management">Version Management</H3>
460 | <CodeBlock language="bash" code={`
461 | # Update version
462 | npm version patch # 1.0.0 → 1.0.1
463 | npm version minor # 1.0.0 → 1.1.0
464 | npm version major # 1.0.0 → 2.0.0
465 |
466 | # Push tags
467 | git push origin main --tags
468 | `} />
469 |
470 | <H3 id="release-checklist">Release Checklist</H3>
471 | <p><strong>1. Update Documentation</strong></p>
472 | <ul className="list-disc pl-6 space-y-1">
473 | <li>README.md tool counts & feature surface (36+ phrasing)</li>
474 | <li><InlineCode>ai-instructions/github-copilot/copilot-instructions.md</InlineCode> architecture updates</li>
475 | <li><InlineCode>.github/copilot-instructions.md</InlineCode> (sync architecture + counts)</li>
476 | <li>Configuration & Features pages updated if capability surface changed</li>
477 | <li>CHANGELOG.md entry (if present)</li>
478 | </ul>
479 |
480 | <p><strong>2. Testing</strong></p>
481 | <ul className="list-disc pl-6 space-y-1">
482 | <li>All unit tests pass (<InlineCode>npm test</InlineCode>)</li>
483 | <li>Linting passes (<InlineCode>npm run lint:check</InlineCode>)</li>
484 | <li>Manual testing with real SFCC instance</li>
485 | <li>Documentation-only mode validation</li>
486 | <li>Build succeeds (<InlineCode>npm run build</InlineCode>)</li>
487 | </ul>
488 |
489 | <p><strong>3. Build & Package</strong></p>
490 | <ul className="list-disc pl-6 space-y-1">
491 | <li>TypeScript compilation successful</li>
492 | <li>Package size reasonable</li>
493 | <li>Dependencies audit clean (<InlineCode>npm audit</InlineCode>)</li>
494 | </ul>
495 |
496 | <p><strong>4. Release</strong></p>
497 | <ul className="list-disc pl-6 space-y-1">
498 | <li>GitHub release with changelog</li>
499 | <li>npm publish (automated via <InlineCode>.github/workflows/publish.yml</InlineCode>)</li>
500 | <li>Documentation deployment (automated)</li>
501 | </ul>
502 |
503 | <H2 id="contributing-guidelines">🤝 Contributing Guidelines</H2>
504 |
505 | <H3 id="before-contributing">Before Contributing</H3>
506 | <ol className="list-decimal pl-6 space-y-1">
507 | <li><strong>Check Existing Issues</strong>: Search for existing issues or discussions</li>
508 | <li><strong>Discuss Large Changes</strong>: Open an issue for significant modifications</li>
509 | <li><strong>Follow Conventions</strong>: Adhere to established coding and commit patterns</li>
510 | </ol>
511 |
512 | <H3 id="pull-request-process">Pull Request Process</H3>
513 | <ol className="list-decimal pl-6 space-y-1">
514 | <li><strong>Fork & Branch</strong>: Create feature branch from <InlineCode>develop</InlineCode></li>
515 | <li><strong>Implement Changes</strong>: Follow coding standards and testing requirements</li>
516 | <li><strong>Update Documentation</strong>: Ensure documentation reflects changes</li>
517 | <li><strong>Test Thoroughly</strong>: All tests must pass (<InlineCode>npm test</InlineCode>, <InlineCode>npm run lint:check</InlineCode>)</li>
518 | <li><strong>Submit PR</strong>: Provide clear description and link to related issues</li>
519 | </ol>
520 |
521 | <H3 id="code-review">Code Review</H3>
522 | <ul className="list-disc pl-6 space-y-1">
523 | <li><strong>GitHub Actions</strong>: CI pipeline must pass (see <InlineCode>.github/workflows/ci.yml</InlineCode>)</li>
524 | <li><strong>Code Quality</strong>: ESLint and TypeScript checks must pass</li>
525 | <li><strong>Test Coverage</strong>: Maintain or improve test coverage</li>
526 | <li><strong>Documentation</strong>: Ensure user-facing changes are documented</li>
527 | </ul>
528 |
529 | <H2 id="performance-considerations">📊 Performance Considerations</H2>
530 |
531 | <H3 id="optimization-guidelines">Optimization Guidelines</H3>
532 | <ul className="list-disc pl-6 space-y-1">
533 | <li><strong>Caching Strategy</strong>: Implement intelligent caching for API responses</li>
534 | <li><strong>Rate Limiting</strong>: Respect SFCC API limits and implement backoff</li>
535 | <li><strong>Memory Management</strong>: Monitor memory usage, especially for large datasets</li>
536 | <li><strong>Asynchronous Operations</strong>: Use proper async/await patterns</li>
537 | </ul>
538 |
539 | <H3 id="monitoring">Monitoring</H3>
540 | <CodeBlock language="typescript" code={`
541 | // Performance monitoring example
542 | const startTime = Date.now();
543 | try {
544 | const result = await performOperation();
545 | this.metrics.recordSuccess('operation_name', Date.now() - startTime);
546 | return result;
547 | } catch (error) {
548 | this.metrics.recordError('operation_name', Date.now() - startTime);
549 | throw error;
550 | }
551 | `} />
552 |
553 | <H2 id="security-considerations">🔒 Security Considerations</H2>
554 |
555 | <H3 id="credential-handling">Credential Handling</H3>
556 | <ul className="list-disc pl-6 space-y-1">
557 | <li><strong>No Hardcoding</strong>: Never commit credentials to repository</li>
558 | <li><strong>Secure Storage</strong>: Use appropriate credential storage mechanisms</li>
559 | <li><strong>Minimal Permissions</strong>: Request only necessary permissions</li>
560 | <li><strong>Rotation Support</strong>: Design for credential rotation</li>
561 | </ul>
562 |
563 | <H3 id="input-validation">Input Validation</H3>
564 | <CodeBlock language="typescript" code={`
565 | // Validate all inputs using proper TypeScript types
566 | import { ToolResponse, ValidationError } from '../types/types.js';
567 |
568 | interface ToolParams {
569 | readonly query: string;
570 | readonly limit?: number;
571 | }
572 |
573 | function validateToolInput(input: unknown): ToolParams {
574 | if (!input || typeof input !== 'object') {
575 | throw new ValidationError('Input must be an object');
576 | }
577 |
578 | const { query, limit } = input as any;
579 |
580 | if (!query || typeof query !== 'string') {
581 | throw new ValidationError('Query is required and must be a string');
582 | }
583 |
584 | if (query.length > 1000) {
585 | throw new ValidationError('Query must be 1000 characters or less');
586 | }
587 |
588 | if (limit !== undefined && (typeof limit !== 'number' || limit < 1 || limit > 100)) {
589 | throw new ValidationError('Limit must be a number between 1 and 100');
590 | }
591 |
592 | return { query, limit };
593 | }
594 | `} />
595 |
596 | <H2 id="next-steps">Next Steps</H2>
597 | <ul className="list-disc pl-6 space-y-2">
598 | <li>📝 <strong><a href="https://github.com/taurgis/sfcc-dev-mcp/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer">Contributing Guidelines</a></strong> - Detailed contribution process</li>
599 | <li>🏗️ <strong><a href="https://github.com/taurgis/sfcc-dev-mcp/issues" target="_blank" rel="noopener noreferrer">Issues & Features</a></strong> - Report bugs or request features</li>
600 | <li>💬 <strong><a href="https://github.com/taurgis/sfcc-dev-mcp/discussions" target="_blank" rel="noopener noreferrer">Discussions</a></strong> - Community discussions and Q&A</li>
601 | <li>🚀 <strong><a href="https://github.com/taurgis/sfcc-dev-mcp/actions" target="_blank" rel="noopener noreferrer">GitHub Actions</a></strong> - View CI/CD pipeline status</li>
602 | </ul>
603 | </div>
604 | );
605 | };
606 |
607 | export default DevelopmentPage;
```