diff --git a/packages/b2c-dx-mcp/src/commands/mcp.ts b/packages/b2c-dx-mcp/src/commands/mcp.ts index 9696cf2a..00943a21 100644 --- a/packages/b2c-dx-mcp/src/commands/mcp.ts +++ b/packages/b2c-dx-mcp/src/commands/mcp.ts @@ -277,6 +277,20 @@ export default class McpServerCommand extends BaseCommand; * Tools are organized by their declared `toolsets` array, allowing * a single tool to appear in multiple toolsets. * - * @param services - Services instance for dependency injection + * @param loadServices - Function that loads configuration and returns Services instance * @returns Complete tool registry */ -export function createToolRegistry(services: Services): ToolRegistry { +export function createToolRegistry(loadServices: () => Services): ToolRegistry { const registry: ToolRegistry = { CARTRIDGES: [], MRT: [], @@ -90,11 +90,11 @@ export function createToolRegistry(services: Services): ToolRegistry { // Collect all tools from all factories const allTools: McpTool[] = [ - ...createCartridgesTools(services), - ...createMrtTools(services), - ...createPwav3Tools(services), - ...createScapiTools(services), - ...createStorefrontNextTools(services), + ...createCartridgesTools(loadServices), + ...createMrtTools(loadServices), + ...createPwav3Tools(loadServices), + ...createScapiTools(loadServices), + ...createStorefrontNextTools(loadServices), ]; // Organize tools by their declared toolsets (supports multi-toolset) @@ -167,16 +167,20 @@ async function performAutoDiscovery(flags: StartupFlags, reason: string): Promis * * @param flags - Startup flags from CLI * @param server - B2CDxMcpServer instance - * @param services - Services instance + * @param loadServices - Function that loads configuration and returns Services instance */ -export async function registerToolsets(flags: StartupFlags, server: B2CDxMcpServer, services: Services): Promise { +export async function registerToolsets( + flags: StartupFlags, + server: B2CDxMcpServer, + loadServices: () => Services, +): Promise { const toolsets = flags.toolsets ?? []; const individualTools = flags.tools ?? []; const allowNonGaTools = flags.allowNonGaTools ?? false; const logger = getLogger(); // Create the tool registry (all available tools) - const toolRegistry = createToolRegistry(services); + const toolRegistry = createToolRegistry(loadServices); // Build flat list of all tools for lookup const allTools = Object.values(toolRegistry).flat(); diff --git a/packages/b2c-dx-mcp/src/services.ts b/packages/b2c-dx-mcp/src/services.ts index 22ed5374..4a3d617a 100644 --- a/packages/b2c-dx-mcp/src/services.ts +++ b/packages/b2c-dx-mcp/src/services.ts @@ -313,6 +313,19 @@ export class Services { return this.b2cInstance.webdav; } + /** + * Get the project working directory. + * Falls back to process.cwd() if not explicitly set. + * + * This is the directory where the project is located, which may differ from process.cwd() + * when MCP clients spawn servers from a different location (e.g., home directory). + * + * @returns Project working directory path + */ + public getWorkingDirectory(): string { + return this.resolvedConfig.values.workingDirectory ?? process.cwd(); + } + /** * Join path segments. * diff --git a/packages/b2c-dx-mcp/src/tools/adapter.ts b/packages/b2c-dx-mcp/src/tools/adapter.ts index 55d9eb65..55a9b412 100644 --- a/packages/b2c-dx-mcp/src/tools/adapter.ts +++ b/packages/b2c-dx-mcp/src/tools/adapter.ts @@ -9,22 +9,22 @@ * * This module provides utilities for creating standardized MCP tools that: * - Validate input using Zod schemas - * - Inject pre-resolved B2CInstance for WebDAV/OCAPI operations (requiresInstance) - * - Inject pre-resolved MRT auth for MRT API operations (requiresMrtAuth) + * - Inject loaded B2CInstance for WebDAV/OCAPI operations (requiresInstance) + * - Inject loaded MRT auth for MRT API operations (requiresMrtAuth) * - Format output consistently (textResult, jsonResult, errorResult) * * ## Configuration Resolution * - * Both B2C instance and MRT auth are resolved once at server startup via - * {@link Services.fromResolvedConfig} and reused for all tool calls: + * Both B2C instance and MRT auth are loaded before each tool call via + * a loader function that calls {@link Services.fromResolvedConfig}: * - * - **B2CInstance**: Resolved from flags + dw.json. Available when `requiresInstance: true`. - * - **MRT Auth**: Resolved from --api-key → SFCC_MRT_API_KEY → ~/.mobify. Available when `requiresMrtAuth: true`. + * - **B2CInstance**: Loaded from flags + dw.json on each call. Available when `requiresInstance: true`. + * - **MRT Auth**: Loaded from --api-key → SFCC_MRT_API_KEY → ~/.mobify on each call. Available when `requiresMrtAuth: true`. * - * This "resolve eagerly at startup" pattern provides: - * - Fail-fast behavior (configuration errors surface at startup) - * - Consistent mental model (both resolved the same way) - * - Better performance (no resolution on each tool call) + * This "load on each call" pattern provides: + * - Fresh configuration on each tool invocation (picks up changes to config files) + * - Consistent mental model (both loaded the same way) + * - Tools can respond to configuration changes without server restart * * @module tools/adapter * @@ -48,8 +48,11 @@ * * @example MRT tool (MRT API) * ```typescript - * // Services created from already-resolved config at startup - * const services = Services.fromResolvedConfig(this.resolvedConfig); + * // Loader function that loads config and creates Services on each tool call + * const loadServices = () => { + * const config = this.loadConfiguration(); + * return Services.fromResolvedConfig(config); + * }; * * const mrtTool = createToolAdapter({ * name: 'mrt_bundle_push', @@ -59,12 +62,12 @@ * inputSchema: { * projectSlug: z.string().describe('MRT project slug'), * }, - * execute: async (args, { mrtAuth }) => { - * const result = await pushBundle({ projectSlug: args.projectSlug }, mrtAuth); + * execute: async (args, { mrtConfig }) => { + * const result = await pushBundle({ projectSlug: args.projectSlug }, mrtConfig.auth); * return result; * }, * formatOutput: (output) => jsonResult(output), - * }, services); + * }, loadServices); * ``` */ @@ -87,7 +90,7 @@ export interface ToolExecutionContext { /** * MRT configuration (auth, project, environment, origin). - * Pre-resolved at server startup. + * Loaded before each tool call. * Only populated when requiresMrtAuth is true. */ mrtConfig?: MrtConfig; @@ -222,7 +225,7 @@ function formatZodErrors(error: z.ZodError): string { * @template TInput - The validated input type (inferred from inputSchema) * @template TOutput - The output type from the execute function * @param options - Tool adapter configuration - * @param services - Services instance for dependency injection + * @param loadServices - Function that loads configuration and returns Services instance * @returns An McpTool ready for registration * * @example @@ -230,23 +233,28 @@ function formatZodErrors(error: z.ZodError): string { * import { z } from 'zod'; * import { createToolAdapter, jsonResult, errorResult } from './adapter.js'; * + * const loadServices = () => { + * const config = this.loadConfiguration(); + * return Services.fromResolvedConfig(config); + * }; + * * const listCodeVersionsTool = createToolAdapter({ * name: 'code_version_list', * description: 'List all code versions on the instance', * toolsets: ['CARTRIDGES'], * inputSchema: {}, - * execute: async (_args, { instance }) => { - * const result = await instance.ocapi.GET('/code_versions', {}); + * execute: async (_args, { b2cInstance }) => { + * const result = await b2cInstance.ocapi.GET('/code_versions', {}); * if (result.error) throw new Error(result.error.message); * return result.data; * }, * formatOutput: (data) => jsonResult(data), - * }, services); + * }, loadServices); * ``` */ export function createToolAdapter( options: ToolAdapterOptions, - services: Services, + loadServices: () => Services, ): McpTool { const { name, @@ -279,7 +287,10 @@ export function createToolAdapter( const args = parseResult.data as TInput; try { - // 2. Get B2CInstance if required (pre-resolved at startup) + // 2. Load Services to get fresh configuration (re-reads config files) + const services = loadServices(); + + // 3. Get B2CInstance if required (loaded on each call) let b2cInstance: B2CInstance | undefined; if (requiresInstance) { if (!services.b2cInstance) { @@ -290,7 +301,7 @@ export function createToolAdapter( b2cInstance = services.b2cInstance; } - // 3. Get MRT config if required (pre-resolved at startup) + // 4. Get MRT config if required (loaded on each call) let mrtConfig: ToolExecutionContext['mrtConfig']; if (requiresMrtAuth) { if (!services.mrtConfig.auth) { @@ -306,7 +317,7 @@ export function createToolAdapter( }; } - // 4. Execute the operation + // 5. Execute the operation const context: ToolExecutionContext = { b2cInstance, mrtConfig, @@ -314,7 +325,7 @@ export function createToolAdapter( }; const output = await execute(args, context); - // 5. Format output + // 6. Format output return formatOutput(output); } catch (error) { // Handle execution errors diff --git a/packages/b2c-dx-mcp/src/tools/cartridges/index.ts b/packages/b2c-dx-mcp/src/tools/cartridges/index.ts index 28b6e1f1..ddbecf3b 100644 --- a/packages/b2c-dx-mcp/src/tools/cartridges/index.ts +++ b/packages/b2c-dx-mcp/src/tools/cartridges/index.ts @@ -52,11 +52,11 @@ interface CartridgeToolInjections { * 3. Uploads the zip to WebDAV and triggers server-side unzip * 4. Optionally reloads the code version after deploy * - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @param injections - Optional dependency injections for testing * @returns The cartridge_deploy tool */ -function createCartridgeDeployTool(services: Services, injections?: CartridgeToolInjections): McpTool { +function createCartridgeDeployTool(loadServices: () => Services, injections?: CartridgeToolInjections): McpTool { const findAndDeployCartridgesFn = injections?.findAndDeployCartridges || findAndDeployCartridges; return createToolAdapter( { @@ -106,7 +106,7 @@ function createCartridgeDeployTool(services: Services, injections?: CartridgeToo const instance = context.b2cInstance!; // Default directory to current directory - const directory = args.directory || '.'; + const directory = args.directory || context.services.getWorkingDirectory(); // Parse options const options: DeployOptions = { @@ -134,17 +134,17 @@ function createCartridgeDeployTool(services: Services, injections?: CartridgeToo }, formatOutput: (output) => jsonResult(output), }, - services, + loadServices, ); } /** * Creates all tools for the CARTRIDGES toolset. * - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @param injections - Optional dependency injections for testing * @returns Array of MCP tools */ -export function createCartridgesTools(services: Services, injections?: CartridgeToolInjections): McpTool[] { - return [createCartridgeDeployTool(services, injections)]; +export function createCartridgesTools(loadServices: () => Services, injections?: CartridgeToolInjections): McpTool[] { + return [createCartridgeDeployTool(loadServices, injections)]; } diff --git a/packages/b2c-dx-mcp/src/tools/mrt/index.ts b/packages/b2c-dx-mcp/src/tools/mrt/index.ts index 13a126c4..879fee68 100644 --- a/packages/b2c-dx-mcp/src/tools/mrt/index.ts +++ b/packages/b2c-dx-mcp/src/tools/mrt/index.ts @@ -12,6 +12,7 @@ * @module tools/mrt */ +import path from 'node:path'; import {z} from 'zod'; import type {McpTool} from '../../utils/index.js'; import type {Services} from '../../services.js'; @@ -51,11 +52,11 @@ interface MrtToolInjections { * Expects the project to already be built (e.g., `npm run build` completed). * Shared across MRT, PWAV3, and STOREFRONTNEXT toolsets. * - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @param injections - Optional dependency injections for testing * @returns The mrt_bundle_push tool */ -function createMrtBundlePushTool(services: Services, injections?: MrtToolInjections): McpTool { +function createMrtBundlePushTool(loadServices: () => Services, injections?: MrtToolInjections): McpTool { const pushBundleFn = injections?.pushBundle || pushBundle; return createToolAdapter( { @@ -96,7 +97,7 @@ function createMrtBundlePushTool(services: Services, injections?: MrtToolInjecti // Parse comma-separated glob patterns (same as CLI defaults) const ssrOnly = (args.ssrOnly || 'ssr.js,ssr.mjs,server/**/*').split(',').map((s) => s.trim()); const ssrShared = (args.ssrShared || 'static/**/*,client/**/*').split(',').map((s) => s.trim()); - const buildDirectory = args.buildDirectory || './build'; + const buildDirectory = args.buildDirectory || path.join(context.services.getWorkingDirectory(), 'build'); // Log all computed variables before pushing bundle const logger = getLogger(); @@ -132,17 +133,17 @@ function createMrtBundlePushTool(services: Services, injections?: MrtToolInjecti }, formatOutput: (output) => jsonResult(output), }, - services, + loadServices, ); } /** * Creates all tools for the MRT toolset. * - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @param injections - Optional dependency injections for testing * @returns Array of MCP tools */ -export function createMrtTools(services: Services, injections?: MrtToolInjections): McpTool[] { - return [createMrtBundlePushTool(services, injections)]; +export function createMrtTools(loadServices: () => Services, injections?: MrtToolInjections): McpTool[] { + return [createMrtBundlePushTool(loadServices, injections)]; } diff --git a/packages/b2c-dx-mcp/src/tools/pwav3/index.ts b/packages/b2c-dx-mcp/src/tools/pwav3/index.ts index 1aa34f3d..39d0e04b 100644 --- a/packages/b2c-dx-mcp/src/tools/pwav3/index.ts +++ b/packages/b2c-dx-mcp/src/tools/pwav3/index.ts @@ -48,10 +48,15 @@ interface PlaceholderOutput { * @param name - Tool name * @param description - Tool description * @param toolsets - Toolsets this tool belongs to - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @returns The configured MCP tool */ -function createPlaceholderTool(name: string, description: string, toolsets: Toolset[], services: Services): McpTool { +function createPlaceholderTool( + name: string, + description: string, + toolsets: Toolset[], + loadServices: () => Services, +): McpTool { return createToolAdapter( { name, @@ -76,7 +81,7 @@ function createPlaceholderTool(name: string, description: string, toolsets: Tool }, formatOutput: (output) => jsonResult(output), }, - services, + loadServices, ); } @@ -87,44 +92,54 @@ function createPlaceholderTool(name: string, description: string, toolsets: Tool * toolsets: ["MRT", "PWAV3", "STOREFRONTNEXT"] and will * automatically appear in PWAV3. * - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @returns Array of MCP tools */ -export function createPwav3Tools(services: Services): McpTool[] { +export function createPwav3Tools(loadServices: () => Services): McpTool[] { return [ // PWA Kit development tools - createPlaceholderTool('pwakit_create_storefront', 'Create a new PWA Kit storefront project', ['PWAV3'], services), - createPlaceholderTool('pwakit_create_page', 'Create a new page component in PWA Kit project', ['PWAV3'], services), + createPlaceholderTool( + 'pwakit_create_storefront', + 'Create a new PWA Kit storefront project', + ['PWAV3'], + loadServices, + ), + createPlaceholderTool( + 'pwakit_create_page', + 'Create a new page component in PWA Kit project', + ['PWAV3'], + loadServices, + ), createPlaceholderTool( 'pwakit_create_component', 'Create a new React component in PWA Kit project', ['PWAV3'], - services, + loadServices, ), createPlaceholderTool( 'pwakit_get_dev_guidelines', 'Get PWA Kit development guidelines and best practices', ['PWAV3'], - services, + loadServices, ), createPlaceholderTool( 'pwakit_recommend_hooks', 'Recommend appropriate React hooks for PWA Kit use cases', ['PWAV3'], - services, + loadServices, ), - createPlaceholderTool('pwakit_run_site_test', 'Run site tests for PWA Kit project', ['PWAV3'], services), + createPlaceholderTool('pwakit_run_site_test', 'Run site tests for PWA Kit project', ['PWAV3'], loadServices), createPlaceholderTool( 'pwakit_install_agent_rules', 'Install AI agent rules for PWA Kit development', ['PWAV3'], - services, + loadServices, ), createPlaceholderTool( 'pwakit_explore_scapi_shop_api', 'Explore SCAPI Shop API endpoints and capabilities', ['PWAV3'], - services, + loadServices, ), ]; } diff --git a/packages/b2c-dx-mcp/src/tools/scapi/index.ts b/packages/b2c-dx-mcp/src/tools/scapi/index.ts index e2265b8f..db5b253f 100644 --- a/packages/b2c-dx-mcp/src/tools/scapi/index.ts +++ b/packages/b2c-dx-mcp/src/tools/scapi/index.ts @@ -21,9 +21,9 @@ import {createScapiCustomApisStatusTool} from './scapi-custom-apis-status.js'; /** * Creates all tools for the SCAPI toolset. * - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @returns Array of MCP tools */ -export function createScapiTools(services: Services): McpTool[] { - return [createScapiSchemasListTool(services), createScapiCustomApisStatusTool(services)]; +export function createScapiTools(loadServices: () => Services): McpTool[] { + return [createScapiSchemasListTool(loadServices), createScapiCustomApisStatusTool(loadServices)]; } diff --git a/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-apis-status.ts b/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-apis-status.ts index c2051065..acbd0c27 100644 --- a/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-apis-status.ts +++ b/packages/b2c-dx-mcp/src/tools/scapi/scapi-custom-apis-status.ts @@ -128,7 +128,7 @@ interface CustomListOutput { * Mirrors CLI: b2c scapi custom status. All flags supported; agent chooses what to use. * See: https://salesforcecommercecloud.github.io/b2c-developer-tooling/cli/custom-apis.html#b2c-scapi-custom-status */ -export function createScapiCustomApisStatusTool(services: Services): McpTool { +export function createScapiCustomApisStatusTool(loadServices: () => Services): McpTool { return createToolAdapter( { name: 'scapi_custom_apis_status', @@ -202,6 +202,6 @@ CLI: b2c scapi custom status`, }, formatOutput: (output) => jsonResult(output), }, - services, + loadServices, ); } diff --git a/packages/b2c-dx-mcp/src/tools/scapi/scapi-schemas-list.ts b/packages/b2c-dx-mcp/src/tools/scapi/scapi-schemas-list.ts index 9dbbdbd9..f4eb2eaf 100644 --- a/packages/b2c-dx-mcp/src/tools/scapi/scapi-schemas-list.ts +++ b/packages/b2c-dx-mcp/src/tools/scapi/scapi-schemas-list.ts @@ -289,10 +289,10 @@ function getAvailableFilters(schemas: SchemaListItem[]): { * Mirrors CLI: b2c scapi schemas list (discovery) and b2c scapi schemas get (fetch). * Lists or fetches SCAPI schema specifications; includes standard SCAPI and custom API as schema types. * - * @param services - MCP services instance + * @param loadServices - Function that loads configuration and returns Services instance * @returns MCP tool for listing/fetching SCAPI schemas */ -export function createScapiSchemasListTool(services: Services): McpTool { +export function createScapiSchemasListTool(loadServices: () => Services): McpTool { return createToolAdapter( { name: 'scapi_schemas_list', @@ -359,6 +359,6 @@ export function createScapiSchemasListTool(services: Services): McpTool { }, formatOutput: (output) => jsonResult(output), }, - services, + loadServices, ); } diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/developer-guidelines.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/developer-guidelines.ts index 28381387..c96be305 100644 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/developer-guidelines.ts +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/developer-guidelines.ts @@ -100,10 +100,10 @@ const DEFAULT_SECTIONS: SectionKey[] = ['quick-reference', 'data-fetching', 'com /** * Creates the developer guidelines tool for Storefront Next. * - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @returns The configured MCP tool */ -export function createDeveloperGuidelinesTool(services: Services): McpTool { +export function createDeveloperGuidelinesTool(loadServices: () => Services): McpTool { return createToolAdapter( { name: 'storefront_next_development_guidelines', @@ -170,6 +170,6 @@ export function createDeveloperGuidelinesTool(services: Services): McpTool { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); } diff --git a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts index e243a022..9a0c0870 100644 --- a/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts +++ b/packages/b2c-dx-mcp/src/tools/storefrontnext/index.ts @@ -55,10 +55,10 @@ interface PlaceholderOutput { * * @param name - Tool name * @param description - Tool description - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @returns The configured MCP tool */ -function createPlaceholderTool(name: string, description: string, services: Services): McpTool { +function createPlaceholderTool(name: string, description: string, loadServices: () => Services): McpTool { return createToolAdapter( { name, @@ -83,7 +83,7 @@ function createPlaceholderTool(name: string, description: string, services: Serv }, formatOutput: (output) => jsonResult(output), }, - services, + loadServices, ); } @@ -94,37 +94,41 @@ function createPlaceholderTool(name: string, description: string, services: Serv * toolsets: ["MRT", "PWAV3", "STOREFRONTNEXT"] and will * automatically appear in STOREFRONTNEXT. * - * @param services - MCP services + * @param loadServices - Function that loads configuration and returns Services instance * @returns Array of MCP tools */ -export function createStorefrontNextTools(services: Services): McpTool[] { +export function createStorefrontNextTools(loadServices: () => Services): McpTool[] { return [ - createDeveloperGuidelinesTool(services), + createDeveloperGuidelinesTool(loadServices), createPlaceholderTool( 'storefront_next_site_theming', 'Configure and manage site theming for Storefront Next', - services, + loadServices, ), createPlaceholderTool( 'storefront_next_figma_to_component_workflow', 'Convert Figma designs to Storefront Next components', - services, + loadServices, + ), + createPlaceholderTool( + 'storefront_next_generate_component', + 'Generate a new Storefront Next component', + loadServices, ), - createPlaceholderTool('storefront_next_generate_component', 'Generate a new Storefront Next component', services), createPlaceholderTool( 'storefront_next_map_tokens_to_theme', 'Map design tokens to Storefront Next theme configuration', - services, + loadServices, ), createPlaceholderTool( 'storefront_next_design_decorator', 'Apply design decorators to Storefront Next components', - services, + loadServices, ), createPlaceholderTool( 'storefront_next_generate_page_designer_metadata', 'Generate Page Designer metadata for Storefront Next components', - services, + loadServices, ), ]; } diff --git a/packages/b2c-dx-mcp/test/registry.test.ts b/packages/b2c-dx-mcp/test/registry.test.ts index 35d95a66..a8c41fb1 100644 --- a/packages/b2c-dx-mcp/test/registry.test.ts +++ b/packages/b2c-dx-mcp/test/registry.test.ts @@ -11,9 +11,10 @@ import {B2CDxMcpServer} from '../src/server.js'; import type {StartupFlags} from '../src/utils/types.js'; import {createMockResolvedConfig} from './test-helpers.js'; -// Create a mock services instance for testing -function createMockServices(): Services { - return new Services({resolvedConfig: createMockResolvedConfig()}); +// Create a loadServices function for testing +function createMockLoadServicesWrapper(): () => Services { + const services = new Services({resolvedConfig: createMockResolvedConfig()}); + return () => services; } // Create a mock server that tracks registered tools @@ -32,8 +33,8 @@ function createMockServer(): B2CDxMcpServer & {registeredTools: string[]} { describe('registry', () => { describe('createToolRegistry', () => { it('should create a registry with all toolsets', () => { - const services = createMockServices(); - const registry = createToolRegistry(services); + const loadServices = createMockLoadServicesWrapper(); + const registry = createToolRegistry(loadServices); // Verify all expected toolsets exist expect(registry).to.have.property('CARTRIDGES'); @@ -44,8 +45,8 @@ describe('registry', () => { }); it('should create CARTRIDGES tools', () => { - const services = createMockServices(); - const registry = createToolRegistry(services); + const loadServices = createMockLoadServicesWrapper(); + const registry = createToolRegistry(loadServices); expect(registry.CARTRIDGES).to.be.an('array'); expect(registry.CARTRIDGES.length).to.be.greaterThan(0); @@ -55,8 +56,8 @@ describe('registry', () => { }); it('should create MRT tools', () => { - const services = createMockServices(); - const registry = createToolRegistry(services); + const loadServices = createMockLoadServicesWrapper(); + const registry = createToolRegistry(loadServices); expect(registry.MRT).to.be.an('array'); expect(registry.MRT.length).to.be.greaterThan(0); @@ -66,8 +67,8 @@ describe('registry', () => { }); it('should create PWAV3 tools', () => { - const services = createMockServices(); - const registry = createToolRegistry(services); + const loadServices = createMockLoadServicesWrapper(); + const registry = createToolRegistry(loadServices); expect(registry.PWAV3).to.be.an('array'); expect(registry.PWAV3.length).to.be.greaterThan(0); @@ -81,8 +82,8 @@ describe('registry', () => { }); it('should create SCAPI tools', () => { - const services = createMockServices(); - const registry = createToolRegistry(services); + const loadServices = createMockLoadServicesWrapper(); + const registry = createToolRegistry(loadServices); expect(registry.SCAPI).to.be.an('array'); expect(registry.SCAPI.length).to.be.greaterThan(0); @@ -93,8 +94,8 @@ describe('registry', () => { }); it('should create STOREFRONTNEXT tools', () => { - const services = createMockServices(); - const registry = createToolRegistry(services); + const loadServices = createMockLoadServicesWrapper(); + const registry = createToolRegistry(loadServices); expect(registry.STOREFRONTNEXT).to.be.an('array'); expect(registry.STOREFRONTNEXT.length).to.be.greaterThan(0); @@ -108,8 +109,8 @@ describe('registry', () => { }); it('should assign correct toolsets to each tool', () => { - const services = createMockServices(); - const registry = createToolRegistry(services); + const loadServices = createMockLoadServicesWrapper(); + const registry = createToolRegistry(loadServices); // Verify tools have correct toolset assignments for (const tool of registry.CARTRIDGES) { @@ -132,7 +133,6 @@ describe('registry', () => { describe('registerToolsets', () => { it('should auto-discover and register tools when no toolsets or tools provided', async () => { - const services = createMockServices(); const server = createMockServer(); // Use a workspace path that won't match any patterns (should fall back to SCAPI) const flags: StartupFlags = { @@ -142,14 +142,14 @@ describe('registry', () => { // When no flags provided, auto-discovery kicks in // With an unknown project, it falls back to SCAPI toolset - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); expect(server.registeredTools.length).to.be.greaterThan(0); // SCAPI tools should be registered as fallback expect(server.registeredTools).to.include('scapi_schemas_list'); }); it('should skip auto-discovery when empty toolsets array is explicitly provided', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: [], @@ -157,20 +157,21 @@ describe('registry', () => { }; // Empty toolsets array still triggers auto-discovery (length is 0) - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Should have auto-discovered SCAPI as fallback expect(server.registeredTools).to.include('scapi_schemas_list'); }); it('should register tools from a single toolset', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['CARTRIDGES'], allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); expect(server.registeredTools).to.include('cartridge_deploy'); // Should not include tools exclusive to other toolsets @@ -178,14 +179,14 @@ describe('registry', () => { }); it('should register tools from multiple toolsets', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['CARTRIDGES', 'MRT'], allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Should include CARTRIDGES tools expect(server.registeredTools).to.include('cartridge_deploy'); @@ -196,14 +197,14 @@ describe('registry', () => { }); it('should register all toolsets when ALL is specified', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['ALL'], allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Should include tools from all toolsets expect(server.registeredTools).to.include('cartridge_deploy'); @@ -214,14 +215,14 @@ describe('registry', () => { }); it('should register individual tools via --tools flag', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { tools: ['cartridge_deploy', 'mrt_bundle_push'], allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); expect(server.registeredTools).to.have.lengthOf(2); expect(server.registeredTools).to.include('cartridge_deploy'); @@ -229,7 +230,6 @@ describe('registry', () => { }); it('should combine toolsets and individual tools', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['CARTRIDGES'], @@ -237,7 +237,8 @@ describe('registry', () => { allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Should include all CARTRIDGES tools expect(server.registeredTools).to.include('cartridge_deploy'); @@ -248,7 +249,6 @@ describe('registry', () => { }); it('should not duplicate tools when specified in both toolset and --tools', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['CARTRIDGES'], @@ -256,7 +256,8 @@ describe('registry', () => { allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Should only have one instance of cartridge_deploy const cartridgeDeployCount = server.registeredTools.filter((t) => t === 'cartridge_deploy').length; @@ -264,7 +265,6 @@ describe('registry', () => { }); it('should warn and skip invalid tool names', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { tools: ['nonexistent_tool', 'cartridge_deploy'], @@ -272,7 +272,8 @@ describe('registry', () => { }; // Should not throw, just skip invalid tools - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Valid tool should be registered expect(server.registeredTools).to.include('cartridge_deploy'); @@ -281,7 +282,6 @@ describe('registry', () => { }); it('should warn and skip invalid toolset names', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['INVALID_TOOLSET', 'CARTRIDGES'], @@ -289,14 +289,14 @@ describe('registry', () => { }; // Should not throw, just skip invalid toolsets - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Valid toolset's tools should be registered expect(server.registeredTools).to.include('cartridge_deploy'); }); it('should trigger auto-discovery when all toolsets are invalid', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['INVALID1', 'INVALID2'], @@ -304,7 +304,8 @@ describe('registry', () => { }; // Should not throw - triggers auto-discovery as fallback - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Auto-discovery always includes BASE_TOOLSET (SCAPI), even if no project type detected expect(server.registeredTools).to.include('scapi_schemas_list'); @@ -312,7 +313,6 @@ describe('registry', () => { }); it('should trigger auto-discovery when all individual tools are invalid', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { tools: ['nonexistent_tool', 'another_fake_tool'], @@ -320,7 +320,8 @@ describe('registry', () => { }; // Should not throw - triggers auto-discovery as fallback - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Auto-discovery always includes BASE_TOOLSET (SCAPI), even if no project type detected expect(server.registeredTools).to.include('scapi_schemas_list'); @@ -328,14 +329,14 @@ describe('registry', () => { }); it('should skip non-GA tools when allowNonGaTools is false', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['CARTRIDGES'], allowNonGaTools: false, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // All current CARTRIDGES tools are non-GA (isGA: false) // So none should be registered @@ -343,14 +344,14 @@ describe('registry', () => { }); it('should register GA tools even when allowNonGaTools is false', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['ALL'], allowNonGaTools: false, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Currently all tools are non-GA placeholders // This test documents expected behavior for when GA tools exist @@ -359,7 +360,6 @@ describe('registry', () => { describe('auto-discovery', () => { it('should use workingDirectory from flags for detection', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { workingDirectory: '/some/workspace', @@ -367,13 +367,13 @@ describe('registry', () => { }; // Should not throw even with non-existent path - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Falls back to SCAPI for unknown projects expect(server.registeredTools).to.include('scapi_schemas_list'); }); it('should map detected project type to MCP toolsets', async () => { - const services = createMockServices(); const server = createMockServer(); // Use a path that doesn't exist - detection will return 'unknown' project type // which maps to SCAPI toolset @@ -382,14 +382,14 @@ describe('registry', () => { allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Only SCAPI tools should be registered (the fallback for unknown projects) expect(server.registeredTools).to.include('scapi_schemas_list'); }); it('should not auto-discover when individual tools are provided', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { tools: ['cartridge_deploy'], @@ -397,7 +397,8 @@ describe('registry', () => { allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Should only have the explicitly requested tool expect(server.registeredTools).to.have.lengthOf(1); @@ -405,7 +406,6 @@ describe('registry', () => { }); it('should not auto-discover when toolsets are explicitly provided', async () => { - const services = createMockServices(); const server = createMockServer(); const flags: StartupFlags = { toolsets: ['CARTRIDGES'], @@ -413,7 +413,8 @@ describe('registry', () => { allowNonGaTools: true, }; - await registerToolsets(flags, server, services); + const loadServices = createMockLoadServicesWrapper(); + await registerToolsets(flags, server, loadServices); // Should only have CARTRIDGES tools, not auto-discovered toolsets expect(server.registeredTools).to.include('cartridge_deploy'); diff --git a/packages/b2c-dx-mcp/test/services.test.ts b/packages/b2c-dx-mcp/test/services.test.ts index ed988359..77b2b100 100644 --- a/packages/b2c-dx-mcp/test/services.test.ts +++ b/packages/b2c-dx-mcp/test/services.test.ts @@ -172,6 +172,38 @@ describe('services', () => { }); }); + describe('getWorkingDirectory', () => { + it('should return working directory when provided in config', () => { + const workingDir = '/path/to/project'; + const config = createMockResolvedConfig({workingDirectory: workingDir}); + const services = new Services({resolvedConfig: config}); + + expect(services.getWorkingDirectory()).to.equal(workingDir); + }); + + it('should fall back to process.cwd() when not provided', () => { + const config = createMockResolvedConfig(); + const services = new Services({resolvedConfig: config}); + + expect(services.getWorkingDirectory()).to.equal(process.cwd()); + }); + + it('should return working directory from fromResolvedConfig when provided in config', () => { + const workingDir = '/path/to/project'; + const config = createMockResolvedConfig({workingDirectory: workingDir}); + const services = Services.fromResolvedConfig(config); + + expect(services.getWorkingDirectory()).to.equal(workingDir); + }); + + it('should fall back to process.cwd() from fromResolvedConfig when not provided in config', () => { + const config = createMockResolvedConfig(); + const services = Services.fromResolvedConfig(config); + + expect(services.getWorkingDirectory()).to.equal(process.cwd()); + }); + }); + describe('getHomeDir', () => { it('should return home directory', () => { const config = createMockResolvedConfig(); diff --git a/packages/b2c-dx-mcp/test/test-helpers.ts b/packages/b2c-dx-mcp/test/test-helpers.ts index a1745eb4..0b6e5050 100644 --- a/packages/b2c-dx-mcp/test/test-helpers.ts +++ b/packages/b2c-dx-mcp/test/test-helpers.ts @@ -11,6 +11,7 @@ import type {NormalizedConfig, ResolvedB2CConfig} from '@salesforce/b2c-tooling-sdk/config'; import type {B2CInstance} from '@salesforce/b2c-tooling-sdk'; import type {AuthStrategy} from '@salesforce/b2c-tooling-sdk/auth'; +import type {Services} from '../src/services.js'; /** * Creates a minimal mock ResolvedB2CConfig for testing. @@ -43,3 +44,14 @@ export function createMockResolvedConfig(values: Partial = {}) }; return config; } + +/** + * Creates a loadServices function for testing. + * Returns a function that always returns the same Services instance. + * + * @param services - Services instance to return + * @returns Function that returns the Services instance + */ +export function createMockLoadServices(services: Services): () => Services { + return () => services; +} diff --git a/packages/b2c-dx-mcp/test/tools/adapter.test.ts b/packages/b2c-dx-mcp/test/tools/adapter.test.ts index ff29e5b7..ab636e53 100644 --- a/packages/b2c-dx-mcp/test/tools/adapter.test.ts +++ b/packages/b2c-dx-mcp/test/tools/adapter.test.ts @@ -22,6 +22,12 @@ function createMockServices(options?: {mrtAuth?: AuthStrategy}): Services { }); } +// Create a loadServices function for testing +function createMockLoadServices(options?: {mrtAuth?: AuthStrategy}): () => Services { + const services = createMockServices(options); + return () => services; +} + /** * Helper to extract text from a ToolResult. * Throws if the first content item is not a text type. @@ -114,7 +120,7 @@ describe('tools/adapter', () => { describe('createToolAdapter', () => { it('should create a tool with correct metadata', () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -129,7 +135,7 @@ describe('tools/adapter', () => { execute: async () => 'result', formatOutput: (output) => textResult(output), }, - services, + loadServices, ); expect(tool.name).to.equal('test_tool'); @@ -139,7 +145,7 @@ describe('tools/adapter', () => { }); it('should default isGA to true', () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -151,14 +157,14 @@ describe('tools/adapter', () => { execute: async () => 'result', formatOutput: (output) => textResult(output), }, - services, + loadServices, ); expect(tool.isGA).to.be.true; }); it('should validate input using Zod schema', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -173,7 +179,7 @@ describe('tools/adapter', () => { execute: async (args: {name: string; count: number}) => `Received: ${args.name}, ${args.count}`, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); // Test with valid input @@ -194,7 +200,7 @@ describe('tools/adapter', () => { }); it('should return error for invalid input', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -208,7 +214,7 @@ describe('tools/adapter', () => { execute: async (args: {email: string}) => args.email, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({email: 'not-an-email'}); @@ -219,7 +225,7 @@ describe('tools/adapter', () => { }); it('should handle execution errors', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -233,7 +239,7 @@ describe('tools/adapter', () => { }, formatOutput: () => textResult('This should not be reached'), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -244,7 +250,7 @@ describe('tools/adapter', () => { }); it('should handle thrown errors gracefully', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -258,7 +264,7 @@ describe('tools/adapter', () => { }, formatOutput: () => textResult('This should not be reached'), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -269,7 +275,7 @@ describe('tools/adapter', () => { }); it('should pass services to execute function', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); let receivedServices: Services | undefined; const tool = createToolAdapter( @@ -285,16 +291,17 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); await tool.handler({}); + const services = loadServices(); expect(receivedServices).to.equal(services); }); it('should support tools that do not require instance', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); let contextReceived: ToolExecutionContext | undefined; const tool = createToolAdapter( @@ -312,7 +319,7 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({projectName: 'my-storefront'}); @@ -324,7 +331,7 @@ describe('tools/adapter', () => { }); it('should use jsonResult for complex output', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -339,7 +346,7 @@ describe('tools/adapter', () => { }), formatOutput: (output) => jsonResult(output), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -351,7 +358,7 @@ describe('tools/adapter', () => { }); it('should support multiple toolsets', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -363,7 +370,7 @@ describe('tools/adapter', () => { execute: async () => 'multi-toolset result', formatOutput: (output) => textResult(output), }, - services, + loadServices, ); expect(tool.toolsets).to.include('PWAV3'); @@ -371,7 +378,7 @@ describe('tools/adapter', () => { }); it('should handle optional schema fields', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -387,7 +394,7 @@ describe('tools/adapter', () => { `required: ${args.required}, optional: ${args.optional || 'not provided'}`, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); // Without optional field @@ -402,7 +409,7 @@ describe('tools/adapter', () => { }); it('should handle array schema fields', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -416,7 +423,7 @@ describe('tools/adapter', () => { execute: async (args: {items: string[]}) => args.items.join(', '), formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({items: ['a', 'b', 'c']}); @@ -426,7 +433,7 @@ describe('tools/adapter', () => { }); it('should provide detailed validation error messages', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { @@ -441,7 +448,7 @@ describe('tools/adapter', () => { execute: async () => 'success', formatOutput: (output) => textResult(output), }, - services, + loadServices, ); // Test with too short name @@ -452,7 +459,7 @@ describe('tools/adapter', () => { describe('requiresInstance', () => { it('should default requiresInstance to false', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); let contextReceived: ToolExecutionContext | undefined; const tool = createToolAdapter( @@ -467,7 +474,7 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); // Default is now false, so tool should execute without instance @@ -479,8 +486,7 @@ describe('tools/adapter', () => { it('should return error when B2C instance is not configured', async () => { // Services with no b2cInstance (resolution failed or not configured) - const services = createMockServices(); - + const loadServices = createMockLoadServices(); const tool = createToolAdapter( { name: 'bad_config_tool', @@ -491,7 +497,7 @@ describe('tools/adapter', () => { execute: async () => 'should not reach here', formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -510,7 +516,7 @@ describe('tools/adapter', () => { }); it('should default requiresMrtAuth to false', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); let contextReceived: ToolExecutionContext | undefined; const tool = createToolAdapter( @@ -525,7 +531,7 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -535,9 +541,10 @@ describe('tools/adapter', () => { }); it('should provide mrtConfig in context when auth is configured', async () => { - // Use resolveConfig + Services.fromResolvedConfig (simulating what mcp.ts does at startup) + // Use resolveConfig + Services.fromResolvedConfig (simulating what mcp.ts does) const config = resolveConfig({mrtApiKey: 'test-api-key-12345'}); const services = Services.fromResolvedConfig(config); + const loadServices = () => services; let contextReceived: ToolExecutionContext | undefined; const tool = createToolAdapter( @@ -553,7 +560,7 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -569,6 +576,7 @@ describe('tools/adapter', () => { // Note: oclif handles env var fallback for --api-key flag, so we pass mrtApiKey explicitly here const config = resolveConfig({mrtApiKey: 'staging-api-key'}, {cloudOrigin: 'https://cloud-staging.mobify.com'}); const services = Services.fromResolvedConfig(config); + const loadServices = () => services; let contextReceived: ToolExecutionContext | undefined; const tool = createToolAdapter( @@ -584,7 +592,7 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -602,6 +610,7 @@ describe('tools/adapter', () => { mrtOrigin: 'https://custom-cloud.mobify.com', }); const services = Services.fromResolvedConfig(config); + const loadServices = () => services; let contextReceived: ToolExecutionContext | undefined; const tool = createToolAdapter( @@ -617,7 +626,7 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -628,7 +637,7 @@ describe('tools/adapter', () => { }); it('should support both requiresInstance and requiresMrtAuth being false', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); let contextReceived: ToolExecutionContext | undefined; const tool = createToolAdapter( @@ -645,7 +654,7 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -653,12 +662,13 @@ describe('tools/adapter', () => { expect(result.isError).to.be.undefined; expect(contextReceived?.b2cInstance).to.be.undefined; expect(contextReceived?.mrtConfig?.auth).to.be.undefined; + const services = loadServices(); expect(contextReceived?.services).to.equal(services); }); it('should return error when requiresMrtAuth is true but no auth configured', async () => { // No mrtConfig.auth provided to Services - const services = createMockServices({}); + const loadServices = createMockLoadServices({}); const tool = createToolAdapter( { name: 'mrt_no_auth_tool', @@ -671,7 +681,7 @@ describe('tools/adapter', () => { }, formatOutput: (output) => textResult(output), }, - services, + loadServices, ); const result = await tool.handler({}); @@ -684,7 +694,7 @@ describe('tools/adapter', () => { describe('formatOutput variations', () => { it('should allow custom formatOutput logic', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); interface Item { id: number; @@ -709,7 +719,7 @@ describe('tools/adapter', () => { return textResult(`Found ${items.length} items:\n${list}`); }, }, - services, + loadServices, ); // Empty list @@ -729,7 +739,7 @@ describe('tools/adapter', () => { }); it('should allow conditional error/success in formatOutput', async () => { - const services = createMockServices(); + const loadServices = createMockLoadServices(); type OperationResult = {success: boolean; message: string}; @@ -755,7 +765,7 @@ describe('tools/adapter', () => { return textResult(result.message); }, }, - services, + loadServices, ); const successResult = await tool.handler({operation: 'succeed'}); diff --git a/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts b/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts index d35ebffe..335a6819 100644 --- a/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/cartridges/index.test.ts @@ -57,6 +57,14 @@ function createMockServices(options?: {b2cInstance?: B2CInstance}): Services { }); } +/** + * Create a loadServices function for testing. + */ +function createMockLoadServicesWrapper(options?: {b2cInstance?: B2CInstance}): () => Services { + const services = createMockServices(options); + return () => services; +} + describe('tools/cartridges', () => { let sandbox: sinon.SinonSandbox; let findAndDeployCartridgesStub: sinon.SinonStub; @@ -72,8 +80,8 @@ describe('tools/cartridges', () => { describe('createCartridgesTools', () => { it('should create cartridge_deploy tool', () => { - const services = createMockServices(); - const tools = createCartridgesTools(services); + const loadServices = createMockLoadServicesWrapper(); + const tools = createCartridgesTools(loadServices); expect(tools).to.have.lengthOf(1); expect(tools[0].name).to.equal('cartridge_deploy'); @@ -81,13 +89,13 @@ describe('tools/cartridges', () => { }); describe('cartridge_deploy tool metadata', () => { - let services: Services; + let loadServices: () => Services; let tool: ReturnType[0]; beforeEach(() => { const mockInstance = createMockB2CInstance(); - services = createMockServices({b2cInstance: mockInstance}); - tool = createCartridgesTools(services)[0]; + loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + tool = createCartridgesTools(loadServices)[0]; }); it('should have correct tool name', () => { @@ -118,8 +126,6 @@ describe('tools/cartridges', () => { describe('cartridge_deploy execution', () => { it('should call findAndDeployCartridges with instance and default directory', async () => { - const directory = '.'; - const mockResult: DeployResult = { cartridges: [{name: 'app_storefront_base', src: '/path/to/app_storefront_base', dest: 'app_storefront_base'}], codeVersion: 'v1', @@ -128,8 +134,8 @@ describe('tools/cartridges', () => { findAndDeployCartridgesStub.resolves(mockResult); const mockInstance = createMockB2CInstance(); - const services = createMockServices({b2cInstance: mockInstance}); - const tool = createCartridgesTools(services, { + const loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + const tool = createCartridgesTools(loadServices, { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; @@ -143,7 +149,8 @@ describe('tools/cartridges', () => { DeployOptions, ]; expect(instance).to.equal(mockInstance); - expect(dir).to.equal(directory); + const services = loadServices(); + expect(dir).to.equal(services.getWorkingDirectory()); expect(options.include).to.be.undefined; expect(options.exclude).to.be.undefined; expect(options.reload).to.be.undefined; @@ -163,8 +170,8 @@ describe('tools/cartridges', () => { findAndDeployCartridgesStub.resolves(mockResult); const mockInstance = createMockB2CInstance(); - const services = createMockServices({b2cInstance: mockInstance}); - const tool = createCartridgesTools(services, { + const loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + const tool = createCartridgesTools(loadServices, { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; @@ -192,8 +199,8 @@ describe('tools/cartridges', () => { findAndDeployCartridgesStub.resolves(mockResult); const mockInstance = createMockB2CInstance(); - const services = createMockServices({b2cInstance: mockInstance}); - const tool = createCartridgesTools(services, { + const loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + const tool = createCartridgesTools(loadServices, { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; @@ -216,8 +223,8 @@ describe('tools/cartridges', () => { findAndDeployCartridgesStub.resolves(mockResult); const mockInstance = createMockB2CInstance(); - const services = createMockServices({b2cInstance: mockInstance}); - const tool = createCartridgesTools(services, { + const loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + const tool = createCartridgesTools(loadServices, { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; @@ -238,8 +245,8 @@ describe('tools/cartridges', () => { findAndDeployCartridgesStub.resolves(mockResult); const mockInstance = createMockB2CInstance(); - const services = createMockServices({b2cInstance: mockInstance}); - const tool = createCartridgesTools(services, { + const loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + const tool = createCartridgesTools(loadServices, { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; @@ -265,8 +272,8 @@ describe('tools/cartridges', () => { findAndDeployCartridgesStub.resolves(mockResult); const mockInstance = createMockB2CInstance(); - const services = createMockServices({b2cInstance: mockInstance}); - const tool = createCartridgesTools(services, { + const loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + const tool = createCartridgesTools(loadServices, { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; @@ -297,8 +304,8 @@ describe('tools/cartridges', () => { findAndDeployCartridgesStub.resolves(mockResult); const mockInstance = createMockB2CInstance(); - const services = createMockServices({b2cInstance: mockInstance}); - const tool = createCartridgesTools(services, { + const loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + const tool = createCartridgesTools(loadServices, { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; @@ -316,10 +323,10 @@ describe('tools/cartridges', () => { describe('cartridge_deploy error handling', () => { it('should return error when instance is not configured', async () => { - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ // No instance configured }); - const tool = createCartridgesTools(services)[0]; + const tool = createCartridgesTools(loadServices)[0]; const result = await tool.handler({}); @@ -335,8 +342,8 @@ describe('tools/cartridges', () => { findAndDeployCartridgesStub.rejects(error); const mockInstance = createMockB2CInstance(); - const services = createMockServices({b2cInstance: mockInstance}); - const tool = createCartridgesTools(services, { + const loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + const tool = createCartridgesTools(loadServices, { findAndDeployCartridges: findAndDeployCartridgesStub, })[0]; @@ -350,13 +357,13 @@ describe('tools/cartridges', () => { }); describe('cartridge_deploy input validation', () => { - let services: Services; + let loadServices: () => Services; let tool: ReturnType[0]; beforeEach(() => { const mockInstance = createMockB2CInstance(); - services = createMockServices({b2cInstance: mockInstance}); - tool = createCartridgesTools(services)[0]; + loadServices = createMockLoadServicesWrapper({b2cInstance: mockInstance}); + tool = createCartridgesTools(loadServices)[0]; }); it('should validate input schema', async () => { diff --git a/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts b/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts index aa1b9713..b5dc9339 100644 --- a/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts +++ b/packages/b2c-dx-mcp/test/tools/mrt/index.test.ts @@ -77,6 +77,19 @@ function createMockServices(options?: { }); } +/** + * Create a loadServices function for testing. + */ +function createMockLoadServicesWrapper(options?: { + mrtAuth?: AuthStrategy; + mrtProject?: string; + mrtEnvironment?: string; + mrtOrigin?: string; +}): () => Services { + const services = createMockServices(options); + return () => services; +} + describe('tools/mrt', () => { let sandbox: sinon.SinonSandbox; let pushBundleStub: sinon.SinonStub; @@ -92,8 +105,8 @@ describe('tools/mrt', () => { describe('createMrtTools', () => { it('should create mrt_bundle_push tool', () => { - const services = createMockServices(); - const tools = createMrtTools(services); + const loadServices = createMockLoadServicesWrapper(); + const tools = createMrtTools(loadServices); expect(tools).to.have.lengthOf(1); expect(tools[0].name).to.equal('mrt_bundle_push'); @@ -101,12 +114,12 @@ describe('tools/mrt', () => { }); describe('mrt_bundle_push tool metadata', () => { - let services: Services; + let loadServices: () => Services; let tool: ReturnType[0]; beforeEach(() => { - services = createMockServices({mrtAuth: new MockAuthStrategy(), mrtProject: 'test-project'}); - tool = createMrtTools(services)[0]; + loadServices = createMockLoadServicesWrapper({mrtAuth: new MockAuthStrategy(), mrtProject: 'test-project'}); + tool = createMrtTools(loadServices)[0]; }); it('should have correct tool name', () => { @@ -144,11 +157,11 @@ describe('tools/mrt', () => { }; pushBundleStub.resolves(mockResult); - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ mrtAuth: new MockAuthStrategy(), mrtProject: 'my-project', }); - const tool = createMrtTools(services, {pushBundle: pushBundleStub})[0]; + const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; const result = await tool.handler({buildDirectory: buildDir}); @@ -174,12 +187,12 @@ describe('tools/mrt', () => { }; pushBundleStub.resolves(mockResult); - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ mrtAuth: new MockAuthStrategy(), mrtProject: 'my-project', mrtEnvironment: 'staging', }); - const tool = createMrtTools(services, {pushBundle: pushBundleStub})[0]; + const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; const result = await tool.handler({buildDirectory: buildDir}); @@ -205,12 +218,12 @@ describe('tools/mrt', () => { }; pushBundleStub.resolves(mockResult); - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ mrtAuth: new MockAuthStrategy(), mrtProject: 'my-project', mrtOrigin: customOrigin, }); - const tool = createMrtTools(services, {pushBundle: pushBundleStub})[0]; + const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; const result = await tool.handler({buildDirectory: buildDir}); @@ -234,11 +247,11 @@ describe('tools/mrt', () => { }; pushBundleStub.resolves(mockResult); - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ mrtAuth: new MockAuthStrategy(), mrtProject: 'my-project', }); - const tool = createMrtTools(services, {pushBundle: pushBundleStub})[0]; + const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; await tool.handler({buildDirectory: buildDir, message: 'Custom deployment message'}); @@ -258,11 +271,11 @@ describe('tools/mrt', () => { }; pushBundleStub.resolves(mockResult); - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ mrtAuth: new MockAuthStrategy(), mrtProject: 'my-project', }); - const tool = createMrtTools(services, {pushBundle: pushBundleStub})[0]; + const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; const result = await tool.handler({buildDirectory: buildDir, message: 'Release v1.0.0'}); @@ -277,11 +290,11 @@ describe('tools/mrt', () => { describe('mrt_bundle_push error handling', () => { it('should return error when project is not configured', async () => { - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ mrtAuth: new MockAuthStrategy(), // No project configured }); - const tool = createMrtTools(services)[0]; + const tool = createMrtTools(loadServices)[0]; const result = await tool.handler({}); @@ -293,11 +306,11 @@ describe('tools/mrt', () => { }); it('should return error when requiresMrtAuth is true but no auth configured', async () => { - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ // No auth configured mrtProject: 'my-project', }); - const tool = createMrtTools(services)[0]; + const tool = createMrtTools(loadServices)[0]; const result = await tool.handler({}); @@ -314,11 +327,11 @@ describe('tools/mrt', () => { const error = new Error('Failed to push bundle: Network error'); pushBundleStub.rejects(error); - const services = createMockServices({ + const loadServices = createMockLoadServicesWrapper({ mrtAuth: new MockAuthStrategy(), mrtProject: 'my-project', }); - const tool = createMrtTools(services, {pushBundle: pushBundleStub})[0]; + const tool = createMrtTools(loadServices, {pushBundle: pushBundleStub})[0]; const result = await tool.handler({buildDirectory: buildDir}); @@ -330,12 +343,12 @@ describe('tools/mrt', () => { }); describe('mrt_bundle_push input validation', () => { - let services: Services; + let loadServices: () => Services; let tool: ReturnType[0]; beforeEach(() => { - services = createMockServices({mrtAuth: new MockAuthStrategy(), mrtProject: 'my-project'}); - tool = createMrtTools(services)[0]; + loadServices = createMockLoadServicesWrapper({mrtAuth: new MockAuthStrategy(), mrtProject: 'my-project'}); + tool = createMrtTools(loadServices)[0]; }); it('should validate input schema', async () => { diff --git a/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts b/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts index 0498f0b0..ed314744 100644 --- a/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts +++ b/packages/b2c-dx-mcp/test/tools/scapi/scapi-custom-apis-status.test.ts @@ -94,7 +94,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { describe('createScapiCustomApisStatusTool', () => { it('should create scapi_custom_apis_status tool with correct metadata', () => { - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); expect(tool).to.exist; expect(tool.name).to.equal('scapi_custom_apis_status'); @@ -109,7 +109,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { }); it('should have optional input params: status, groupBy, columns', () => { - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); expect(tool.inputSchema).to.have.property('status'); expect(tool.inputSchema).to.have.property('groupBy'); @@ -123,7 +123,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { const mockEndpoints = createMockEndpoints(); mockGet.resolves(createMockClientResponse(mockEndpoints, 'version1')); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({}); expect(result.isError).to.be.undefined; @@ -149,7 +149,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { it('should pass status filter to SDK when provided', async () => { mockGet.resolves(createMockClientResponse([])); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); await tool.handler({status: 'active'}); expect(mockGet.calledOnce).to.be.true; @@ -163,7 +163,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { ]); mockGet.resolves(createMockClientResponse(mockEndpoints)); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({}); const {parsed} = parseResultContent(result); @@ -178,7 +178,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { it('should return empty endpoints and message when no data returned', async () => { mockGet.resolves(createMockClientResponse([], 'v1')); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({}); const {parsed} = parseResultContent(result); @@ -194,7 +194,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { response: {status: 400, statusText: 'Bad Request'}, }); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({}); const {parsed} = parseResultContent(result); @@ -206,7 +206,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { it('should handle SDK exceptions and return remoteError', async () => { mockGet.rejects(new Error('Network error')); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({}); const {parsed} = parseResultContent(result); @@ -221,7 +221,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { ]); mockGet.resolves(createMockClientResponse(mockEndpoints)); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({groupBy: 'type'}); const {parsed} = parseResultContent(result); @@ -240,7 +240,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { ]); mockGet.resolves(createMockClientResponse(mockEndpoints)); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({groupBy: 'site'}); const {parsed} = parseResultContent(result); @@ -255,7 +255,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { const mockEndpoints = createMockEndpoints(); mockGet.resolves(createMockClientResponse(mockEndpoints)); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({columns: 'type,apiName,status'}); const {parsed} = parseResultContent(result); @@ -278,7 +278,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { ]); mockGet.resolves(createMockClientResponse(mockEndpoints)); - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({ columns: 'type,apiName,apiVersion,cartridgeName,endpointPath,httpMethod,status,siteId,securityScheme,operationId,schemaFile,implementationScript,errorReason,id', @@ -308,7 +308,7 @@ describe('tools/scapi/scapi-custom-apis-status', () => { }); it('should return validation error for invalid status value', async () => { - const tool = createScapiCustomApisStatusTool(services); + const tool = createScapiCustomApisStatusTool(() => services); const result = await tool.handler({status: 'invalid'}); expect(result.isError).to.be.true; diff --git a/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts b/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts index bf58ab0e..e82faf81 100644 --- a/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts +++ b/packages/b2c-dx-mcp/test/tools/scapi/scapi-schemas-list.test.ts @@ -54,7 +54,7 @@ describe('tools/scapi/scapi-schemas-list', () => { describe('createScapiSchemasListTool', () => { it('creates tool with correct metadata', () => { - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); expect(tool).to.exist; expect(tool.name).to.equal('scapi_schemas_list'); @@ -69,7 +69,7 @@ describe('tools/scapi/scapi-schemas-list', () => { }); it('has optional input params: apiFamily, apiName, apiVersion, status, includeSchemas, expandAll', () => { - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); expect(tool.inputSchema).to.have.property('apiFamily'); expect(tool.inputSchema).to.have.property('apiName'); @@ -106,7 +106,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({}); expect(result.isError).to.be.undefined; @@ -139,7 +139,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); await tool.handler({ apiFamily: 'checkout', apiName: 'shopper-baskets', @@ -162,7 +162,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({}); const {parsed} = parseResultContent(result); @@ -179,7 +179,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({apiFamily: 'checkout', status: 'current'}); const {parsed} = parseResultContent(result); @@ -201,7 +201,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({}); const {parsed} = parseResultContent(result); @@ -217,7 +217,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 401, statusText: 'Unauthorized'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({}); expect(result.isError).to.be.true; @@ -236,7 +236,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({ apiFamily: 'product', apiName: 'shopper-products', @@ -276,7 +276,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({ apiFamily: 'checkout', apiName: 'shopper-baskets', @@ -297,7 +297,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({ apiFamily: 'product', apiName: 'shopper-products', @@ -318,7 +318,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 404, statusText: 'Not Found'}, }); - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({ apiFamily: 'product', apiName: 'nonexistent-api', @@ -352,7 +352,7 @@ describe('tools/scapi/scapi-schemas-list', () => { response: {status: 200, statusText: 'OK'}, }); - const tool = createScapiSchemasListTool(servicesWithoutShortCode); + const tool = createScapiSchemasListTool(() => servicesWithoutShortCode); const result = await tool.handler({ apiFamily: 'product', apiName: 'shopper-products', @@ -368,7 +368,7 @@ describe('tools/scapi/scapi-schemas-list', () => { describe('handler (validation and errors)', () => { it('returns error result when includeSchemas true but missing apiFamily', async () => { - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({ apiName: 'shopper-baskets', apiVersion: 'v1', @@ -382,7 +382,7 @@ describe('tools/scapi/scapi-schemas-list', () => { }); it('returns error result when includeSchemas true but missing apiVersion', async () => { - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({ apiFamily: 'checkout', apiName: 'shopper-baskets', @@ -393,7 +393,7 @@ describe('tools/scapi/scapi-schemas-list', () => { }); it('returns validation error for invalid status value', async () => { - const tool = createScapiSchemasListTool(services); + const tool = createScapiSchemasListTool(() => services); const result = await tool.handler({status: 'invalid' as 'current'}); expect(result.isError).to.be.true; diff --git a/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts b/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts index bc566926..d16a9606 100644 --- a/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts +++ b/packages/b2c-dx-mcp/test/tools/storefrontnext/developer-guidelines.test.ts @@ -38,12 +38,12 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('tool metadata', () => { it('should have correct tool name', () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); expect(tool.name).to.equal('storefront_next_development_guidelines'); }); it('should have concise, action-oriented description', () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const desc = tool.description; // Should emphasize this is an essential first step (most important) @@ -70,7 +70,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should list all sections in inputSchema description', () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // The inputSchema should list all available sections for discoverability // This is better UX than burying them in the main description @@ -101,7 +101,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { // Detailed topics should be in inputSchema.sections.describe() // This follows MCP best practices: main description = WHEN/WHY, inputSchema = HOW - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const desc = tool.description; // Main description should be concise, not list all topics @@ -117,18 +117,18 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should be in STOREFRONTNEXT toolset', () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); expect(tool.toolsets).to.include('STOREFRONTNEXT'); expect(tool.toolsets).to.have.lengthOf(1); }); it('should be GA (generally available)', () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); expect(tool.isGA).to.be.false; }); it('should not require B2C instance', () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // Guidelines are static content, no instance needed expect(tool).to.not.have.property('requiresInstance'); }); @@ -154,7 +154,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { ]; // Create tool to verify derived _SECTIONS matches - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // Each section should be valid and retrievable for (const section of allSections) { @@ -166,7 +166,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('inputSchema behavior', () => { it('should have sections parameter that is optional', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // Should work without providing sections parameter const result = await tool.handler({}); @@ -175,7 +175,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should accept array of valid section enums', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // All valid sections from _SECTIONS constant const validSections = [ @@ -203,7 +203,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('default behavior', () => { it('should return comprehensive guidelines by default when no sections specified', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({}); expect(result.isError).to.be.undefined; @@ -226,7 +226,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return empty string when sections array is explicitly empty', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: []}); expect(result.isError).to.be.undefined; @@ -260,7 +260,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return quick-reference section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['quick-reference']}); expect(result.isError).to.be.undefined; @@ -269,7 +269,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return data-fetching section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['data-fetching']}); expect(result.isError).to.be.undefined; @@ -278,7 +278,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return state-management section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['state-management']}); expect(result.isError).to.be.undefined; @@ -287,7 +287,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return auth section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['auth']}); expect(result.isError).to.be.undefined; @@ -296,7 +296,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return config section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['config']}); expect(result.isError).to.be.undefined; @@ -305,7 +305,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return i18n section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['i18n']}); expect(result.isError).to.be.undefined; @@ -314,7 +314,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return components section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['components']}); expect(result.isError).to.be.undefined; @@ -323,7 +323,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return page-designer section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['page-designer']}); expect(result.isError).to.be.undefined; @@ -332,7 +332,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return performance section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['performance']}); expect(result.isError).to.be.undefined; @@ -341,7 +341,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return testing section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['testing']}); expect(result.isError).to.be.undefined; @@ -350,7 +350,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return extensions section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['extensions']}); expect(result.isError).to.be.undefined; @@ -359,7 +359,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return pitfalls section', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['pitfalls']}); expect(result.isError).to.be.undefined; @@ -370,7 +370,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('multiple section retrieval', () => { it('should support contextual learning with multiple sections', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // Test related sections together (as mentioned in description) const result = await tool.handler({ @@ -392,7 +392,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should combine three sections correctly', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({ sections: ['auth', 'config', 'i18n'], }); @@ -410,7 +410,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should maintain order of sections as requested', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // Request sections in specific order const result = await tool.handler({ @@ -438,7 +438,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should handle all sections at once', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({ sections: [ 'quick-reference', @@ -477,7 +477,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('input validation', () => { it('should reject invalid section names', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await tool.handler({sections: ['invalid-section']} as any); @@ -487,7 +487,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should reject empty strings in sections array', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await tool.handler({sections: ['']} as any); @@ -497,7 +497,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should reject non-array sections parameter', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await tool.handler({sections: 'quick-reference'} as any); @@ -509,7 +509,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('content verification', () => { it('should load actual markdown content from files', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['quick-reference']}); expect(result.isError).to.be.undefined; @@ -521,7 +521,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should return different content for different sections', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result1 = await tool.handler({sections: ['data-fetching']}); const result2 = await tool.handler({sections: ['auth']}); @@ -537,7 +537,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should cover critical topics mentioned in description', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // Test that key topics from the description are covered in relevant sections const topicTests = [ @@ -563,7 +563,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should provide non-negotiable architecture rules in quick-reference', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['quick-reference']}); expect(result.isError).to.be.undefined; @@ -582,7 +582,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should emphasize TypeScript-only approach', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: ['quick-reference']}); expect(result.isError).to.be.undefined; @@ -595,7 +595,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { describe('edge cases', () => { it('should handle undefined sections parameter', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({sections: undefined}); expect(result.isError).to.be.undefined; @@ -607,7 +607,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should handle sections parameter explicitly set to null', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); // eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await tool.handler({sections: null} as any); @@ -616,7 +616,7 @@ describe('tools/storefrontnext/developer-guidelines', () => { }); it('should handle duplicate sections in array', async () => { - const tool = createDeveloperGuidelinesTool(services); + const tool = createDeveloperGuidelinesTool(() => services); const result = await tool.handler({ sections: ['auth', 'auth'], }); diff --git a/packages/b2c-plugin-example-config/src/sources/env-file-source.ts b/packages/b2c-plugin-example-config/src/sources/env-file-source.ts index c41c2c9c..0c4980c7 100644 --- a/packages/b2c-plugin-example-config/src/sources/env-file-source.ts +++ b/packages/b2c-plugin-example-config/src/sources/env-file-source.ts @@ -58,10 +58,10 @@ export class EnvFileSource implements ConfigSource { * * File location priority: * 1. B2C_ENV_FILE_PATH environment variable (explicit override) - * 2. .env.b2c in startDir (from options) + * 2. .env.b2c in workingDirectory (from options) * 3. .env.b2c in current working directory * - * @param options - Resolution options (startDir used for file lookup) + * @param options - Resolution options (workingDirectory used for file lookup) * @returns Parsed configuration and location, or undefined if file not found */ load(options: ResolveConfigOptions): ConfigLoadResult | undefined { @@ -71,7 +71,7 @@ export class EnvFileSource implements ConfigSource { if (envOverride) { envFilePath = envOverride; } else { - const searchDir = options.startDir ?? process.cwd(); + const searchDir = options.workingDirectory ?? process.cwd(); envFilePath = join(searchDir, '.env.b2c'); } diff --git a/packages/b2c-tooling-sdk/src/cli/base-command.ts b/packages/b2c-tooling-sdk/src/cli/base-command.ts index 83a2b3c5..53e1e197 100644 --- a/packages/b2c-tooling-sdk/src/cli/base-command.ts +++ b/packages/b2c-tooling-sdk/src/cli/base-command.ts @@ -293,7 +293,7 @@ export abstract class BaseCommand extends Command { * Gets base configuration options from common flags. * * Subclasses should spread these options when overriding loadConfiguration() - * to ensure common options like startDir are always included. + * to ensure common options like workingDirectory are always included. * * @example * ```typescript @@ -310,7 +310,7 @@ export abstract class BaseCommand extends Command { return { instance: this.flags.instance, configPath: this.flags.config, - startDir: this.flags['working-directory'], + workingDirectory: this.flags['working-directory'], }; } diff --git a/packages/b2c-tooling-sdk/src/cli/config.ts b/packages/b2c-tooling-sdk/src/cli/config.ts index fd5cf79b..6086e814 100644 --- a/packages/b2c-tooling-sdk/src/cli/config.ts +++ b/packages/b2c-tooling-sdk/src/cli/config.ts @@ -175,7 +175,7 @@ export interface LoadConfigOptions { /** Explicit path to config file (skips searching if provided) */ configPath?: string; /** Starting directory for config file search (default: current working directory) */ - startDir?: string; + workingDirectory?: string; /** Cloud origin for MRT ~/.mobify lookup (e.g., https://cloud-staging.mobify.com) */ cloudOrigin?: string; /** Path to custom MRT credentials file (overrides default ~/.mobify) */ @@ -236,16 +236,17 @@ export function loadConfig( ): ResolvedB2CConfig { const logger = getLogger(); - // Preserve instanceName from options.instance if not already in flags + // Preserve instanceName and workingDirectory from options if not already in flags const effectiveFlags = { ...flags, instanceName: flags.instanceName ?? options.instance, + workingDirectory: flags.workingDirectory ?? options.workingDirectory, }; const resolved = resolveConfig(effectiveFlags, { instance: options.instance, configPath: options.configPath, - startDir: options.startDir, + workingDirectory: options.workingDirectory, hostnameProtection: true, cloudOrigin: options.cloudOrigin, credentialsFile: options.credentialsFile, diff --git a/packages/b2c-tooling-sdk/src/config/dw-json.ts b/packages/b2c-tooling-sdk/src/config/dw-json.ts index 33db8ed7..f043d4ea 100644 --- a/packages/b2c-tooling-sdk/src/config/dw-json.ts +++ b/packages/b2c-tooling-sdk/src/config/dw-json.ts @@ -96,7 +96,7 @@ export interface LoadDwJsonOptions { /** Explicit path to dw.json (skips searching if provided) */ path?: string; /** Starting directory for search (defaults to cwd) */ - startDir?: string; + workingDirectory?: string; } /** @@ -112,7 +112,7 @@ export interface LoadDwJsonResult { /** * Finds dw.json by searching upward from the starting directory. * - * @param startDir - Directory to start searching from (defaults to cwd) + * @param workingDirectory - Directory to start searching from (defaults to cwd) * @returns Path to dw.json if found, undefined otherwise * * @example @@ -121,8 +121,8 @@ export interface LoadDwJsonResult { * console.log(`Found dw.json at ${dwPath}`); * } */ -export function findDwJson(startDir: string = process.cwd()): string | undefined { - let dir = startDir; +export function findDwJson(workingDirectory: string = process.cwd()): string | undefined { + let dir = workingDirectory; const root = path.parse(dir).root; while (dir !== root) { @@ -209,7 +209,7 @@ function selectConfig(json: DwJsonMultiConfig, instanceName?: string): DwJsonCon */ export function loadFullDwJson(options: LoadDwJsonOptions = {}): {config: DwJsonMultiConfig; path: string} | undefined { const logger = getLogger(); - const dwJsonPath = options.path ?? path.join(options.startDir || process.cwd(), 'dw.json'); + const dwJsonPath = options.path ?? path.join(options.workingDirectory || process.cwd(), 'dw.json'); logger.trace({path: dwJsonPath}, '[DwJsonSource] Checking for config file'); @@ -247,7 +247,7 @@ export interface AddInstanceOptions { /** Path to dw.json (defaults to ./dw.json) */ path?: string; /** Starting directory for search */ - startDir?: string; + workingDirectory?: string; /** Whether to set as active instance */ setActive?: boolean; } @@ -263,7 +263,7 @@ export interface AddInstanceOptions { * @throws Error if instance with same name already exists */ export function addInstance(instance: DwJsonConfig, options: AddInstanceOptions = {}): void { - const dwJsonPath = options.path ?? path.join(options.startDir || process.cwd(), 'dw.json'); + const dwJsonPath = options.path ?? path.join(options.workingDirectory || process.cwd(), 'dw.json'); let existing: DwJsonMultiConfig = {}; if (fs.existsSync(dwJsonPath)) { @@ -322,7 +322,7 @@ export interface RemoveInstanceOptions { /** Path to dw.json */ path?: string; /** Starting directory for search */ - startDir?: string; + workingDirectory?: string; } /** @@ -333,7 +333,7 @@ export interface RemoveInstanceOptions { * @throws Error if instance not found or dw.json doesn't exist */ export function removeInstance(name: string, options: RemoveInstanceOptions = {}): void { - const dwJsonPath = options.path ?? path.join(options.startDir || process.cwd(), 'dw.json'); + const dwJsonPath = options.path ?? path.join(options.workingDirectory || process.cwd(), 'dw.json'); if (!fs.existsSync(dwJsonPath)) { throw new Error('No dw.json file found'); @@ -364,7 +364,7 @@ export interface SetActiveInstanceOptions { /** Path to dw.json */ path?: string; /** Starting directory for search */ - startDir?: string; + workingDirectory?: string; } /** @@ -375,7 +375,7 @@ export interface SetActiveInstanceOptions { * @throws Error if instance not found or dw.json doesn't exist */ export function setActiveInstance(name: string, options: SetActiveInstanceOptions = {}): void { - const dwJsonPath = options.path ?? path.join(options.startDir || process.cwd(), 'dw.json'); + const dwJsonPath = options.path ?? path.join(options.workingDirectory || process.cwd(), 'dw.json'); if (!fs.existsSync(dwJsonPath)) { throw new Error('No dw.json file found'); @@ -418,7 +418,7 @@ export function setActiveInstance(name: string, options: SetActiveInstanceOption * Loads configuration from a dw.json file. * * If an explicit path is provided, uses that file. Otherwise, looks for dw.json - * in the startDir (or cwd). Does NOT search parent directories. + * in the workingDirectory (or cwd). Does NOT search parent directories. * * Use `findDwJson()` if you need to search upward through parent directories. * @@ -434,7 +434,7 @@ export function setActiveInstance(name: string, options: SetActiveInstanceOption * } * * // Load from specific directory - * const result = loadDwJson({ startDir: '/path/to/project' }); + * const result = loadDwJson({ workingDirectory: '/path/to/project' }); * * // Use named instance * const result = loadDwJson({ instance: 'staging' }); @@ -446,7 +446,7 @@ export function loadDwJson(options: LoadDwJsonOptions = {}): LoadDwJsonResult | const logger = getLogger(); // If explicit path provided, use it. Otherwise default to ./dw.json (no upward search) - const dwJsonPath = options.path ?? path.join(options.startDir || process.cwd(), 'dw.json'); + const dwJsonPath = options.path ?? path.join(options.workingDirectory || process.cwd(), 'dw.json'); logger.trace({path: dwJsonPath}, '[DwJsonSource] Checking for config file'); diff --git a/packages/b2c-tooling-sdk/src/config/mapping.ts b/packages/b2c-tooling-sdk/src/config/mapping.ts index 4982e0f5..6315a596 100644 --- a/packages/b2c-tooling-sdk/src/config/mapping.ts +++ b/packages/b2c-tooling-sdk/src/config/mapping.ts @@ -316,6 +316,7 @@ export function mergeConfigsWithProtection( cipHost: overrides.cipHost ?? base.cipHost, sandboxApiHost: overrides.sandboxApiHost ?? base.sandboxApiHost, instanceName: overrides.instanceName ?? base.instanceName, + workingDirectory: overrides.workingDirectory ?? base.workingDirectory, mrtProject: overrides.mrtProject ?? base.mrtProject, mrtEnvironment: overrides.mrtEnvironment ?? base.mrtEnvironment, mrtApiKey: overrides.mrtApiKey ?? base.mrtApiKey, diff --git a/packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts b/packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts index caee5c3b..5350bc2d 100644 --- a/packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts +++ b/packages/b2c-tooling-sdk/src/config/sources/dw-json-source.ts @@ -34,7 +34,7 @@ export class DwJsonSource implements ConfigSource { const result = loadDwJson({ instance: options.instance, path: options.configPath, - startDir: options.startDir, + workingDirectory: options.workingDirectory, }); if (!result) { @@ -55,7 +55,7 @@ export class DwJsonSource implements ConfigSource { listInstances(options?: ResolveConfigOptions): InstanceInfo[] { const result = loadFullDwJson({ path: options?.configPath, - startDir: options?.startDir, + workingDirectory: options?.workingDirectory, }); if (!result) { @@ -101,7 +101,7 @@ export class DwJsonSource implements ConfigSource { const dwJsonConfig = mapNormalizedConfigToDwJson(options.config, options.name); addInstance(dwJsonConfig, { path: options.configPath, - startDir: options.startDir, + workingDirectory: options.workingDirectory, setActive: options.setActive, }); } @@ -112,7 +112,7 @@ export class DwJsonSource implements ConfigSource { removeInstance(name: string, options?: ResolveConfigOptions): void { removeInstance(name, { path: options?.configPath, - startDir: options?.startDir, + workingDirectory: options?.workingDirectory, }); } @@ -122,7 +122,7 @@ export class DwJsonSource implements ConfigSource { setActiveInstance(name: string, options?: ResolveConfigOptions): void { setActiveInstance(name, { path: options?.configPath, - startDir: options?.startDir, + workingDirectory: options?.workingDirectory, }); } } diff --git a/packages/b2c-tooling-sdk/src/config/sources/package-json-source.ts b/packages/b2c-tooling-sdk/src/config/sources/package-json-source.ts index dd11831f..49f9ed00 100644 --- a/packages/b2c-tooling-sdk/src/config/sources/package-json-source.ts +++ b/packages/b2c-tooling-sdk/src/config/sources/package-json-source.ts @@ -60,8 +60,8 @@ export class PackageJsonSource implements ConfigSource { load(options: ResolveConfigOptions): ConfigLoadResult | undefined { const logger = getLogger(); - // Only look in cwd (or startDir if provided) - const searchDir = options.startDir ?? process.cwd(); + // Only look in cwd (or workingDirectory if provided) + const searchDir = options.workingDirectory ?? process.cwd(); const packageJsonPath = path.join(searchDir, 'package.json'); logger.trace({location: packageJsonPath}, '[PackageJsonSource] Checking for package.json'); diff --git a/packages/b2c-tooling-sdk/src/config/types.ts b/packages/b2c-tooling-sdk/src/config/types.ts index 553bef72..bd779a4d 100644 --- a/packages/b2c-tooling-sdk/src/config/types.ts +++ b/packages/b2c-tooling-sdk/src/config/types.ts @@ -79,6 +79,8 @@ export interface NormalizedConfig { // Metadata /** Instance name (from multi-config supporting sources) */ instanceName?: string; + /** Starting directory for config file search and project-relative operations */ + workingDirectory?: string; // TLS/mTLS /** Path to PKCS12 certificate file for client mTLS (two-factor auth) */ @@ -141,7 +143,7 @@ export interface ResolveConfigOptions { /** Explicit path to config file (defaults to auto-discover) */ configPath?: string; /** Starting directory for config file search */ - startDir?: string; + workingDirectory?: string; /** Whether to apply hostname mismatch protection (default: true) */ hostnameProtection?: boolean; /** Cloud origin for ~/.mobify lookup (MRT) */ diff --git a/packages/b2c-tooling-sdk/test/config/sources.test.ts b/packages/b2c-tooling-sdk/test/config/sources.test.ts index 1e8da636..34dbb849 100644 --- a/packages/b2c-tooling-sdk/test/config/sources.test.ts +++ b/packages/b2c-tooling-sdk/test/config/sources.test.ts @@ -552,7 +552,7 @@ describe('config/sources', () => { // Use PackageJsonSource directly to test in isolation const source = new PackageJsonSource(); - const result = source.load({startDir: tempDir}); + const result = source.load({workingDirectory: tempDir}); expect(result).to.not.be.undefined; expect(result!.config.shortCode).to.equal('abc123'); @@ -683,7 +683,7 @@ describe('config/sources', () => { ); const source = new PackageJsonSource(); - const result = source.load({startDir: tempDir}); + const result = source.load({workingDirectory: tempDir}); expect(result).to.not.be.undefined; expect(result!.config.shortCode).to.equal('abc123'); @@ -709,7 +709,7 @@ describe('config/sources', () => { ); const source = new PackageJsonSource(); - const result = source.load({startDir: tempDir}); + const result = source.load({workingDirectory: tempDir}); expect(result).to.not.be.undefined; expect(result!.config.shortCode).to.equal('abc123'); diff --git a/packages/b2c-vs-extension/src/extension.ts b/packages/b2c-vs-extension/src/extension.ts index 81ddfb57..da34fb0e 100644 --- a/packages/b2c-vs-extension/src/extension.ts +++ b/packages/b2c-vs-extension/src/extension.ts @@ -278,12 +278,12 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann type WebDavPropfindEntry = {href: string; displayName?: string; contentLength?: number; isCollection?: boolean}; const listWebDavDisposable = vscode.commands.registerCommand('b2c-dx.listWebDav', async () => { - let startDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd(); - if (!startDir || startDir === '/' || !fs.existsSync(startDir)) { - startDir = context.extensionPath; + let workingDirectory = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? process.cwd(); + if (!workingDirectory || workingDirectory === '/' || !fs.existsSync(workingDirectory)) { + workingDirectory = context.extensionPath; } - const dwPath = findDwJson(startDir); - const config = dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {startDir}); + const dwPath = findDwJson(workingDirectory); + const config = dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {workingDirectory}); if (!config.hasB2CInstanceConfig()) { vscode.window.showErrorMessage( @@ -552,9 +552,9 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann ); let prefill: {tenantId: string; channelId: string; shortCode?: string} | undefined; try { - const startDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; - const dwPath = findDwJson(startDir); - const config = dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {startDir}); + const workingDirectory = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; + const dwPath = findDwJson(workingDirectory); + const config = dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {workingDirectory}); const hostname = config.values.hostname; const shortCode = config.values.shortCode; const firstPart = hostname && typeof hostname === 'string' ? (hostname.split('.')[0] ?? '') : ''; @@ -585,9 +585,9 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann curlText?: string; }) => { const getConfig = () => { - const startDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; - const dwPath = findDwJson(startDir); - return dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {startDir}); + const workingDirectory = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; + const dwPath = findDwJson(workingDirectory); + return dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {workingDirectory}); }; if (msg.type === 'scapiFetchSchemas') { @@ -970,9 +970,9 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann vscode.window.showErrorMessage('B2C DX: Tenant Id and Channel Id are required to create a SLAS client.'); return; } - const startDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; - const dwPath = findDwJson(startDir); - const config = dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {startDir}); + const workingDirectory = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; + const dwPath = findDwJson(workingDirectory); + const config = dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {workingDirectory}); const shortCode = config.values.shortCode; if (!shortCode) { vscode.window.showErrorMessage( @@ -1086,9 +1086,9 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann panel.webview.html = getOdsManagementWebviewContent(context); async function getOdsConfig() { - const startDir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; - const dwPath = findDwJson(startDir); - return dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {startDir}); + const workingDirectory = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? context.extensionPath; + const dwPath = findDwJson(workingDirectory); + return dwPath ? resolveConfig({}, {configPath: dwPath}) : resolveConfig({}, {workingDirectory}); } function realmFromHostname(hostname: string | undefined): string { @@ -1262,7 +1262,7 @@ function activateInner(context: vscode.ExtensionContext, log: vscode.OutputChann const dwPath = findDwJson(projectDirectory); const config = dwPath ? resolveConfig({}, {configPath: dwPath}) - : resolveConfig({}, {startDir: projectDirectory}); + : resolveConfig({}, {workingDirectory: projectDirectory}); if (!config.hasB2CInstanceConfig()) { const message = 'B2C DX: No instance config for deploy. Configure SFCC_* env vars or dw.json in the project.';