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; ```