diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts index c6fb60417fa5..e5b171587024 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -53,6 +53,14 @@ const listProjectsOutputSchema = { 'The prefix to use for component selectors.' + ` For example, a prefix of 'app' would result in selectors like ''.`, ), + unitTestFramework: z + .enum(['jasmine', 'jest', 'vitest', 'unknown']) + .optional() + .describe( + 'The unit test framework used by the project, such as Jasmine, Jest, or Vitest. ' + + 'This field is critical for generating correct and idiomatic unit tests. ' + + 'When writing or modifying tests, you MUST use the APIs corresponding to this framework.', + ), }), ), }), @@ -84,14 +92,14 @@ export const LIST_PROJECTS_TOOL = declareTool({ title: 'List Angular Projects', description: ` -Provides a comprehensive overview of all Angular workspaces and projects within a monorepo. +Provides a comprehensive overview of all Angular workspaces and projects within the repository. It is essential to use this tool as a first step before performing any project-specific actions to understand the available projects, their types, and their locations. * Finding the correct project name to use in other commands (e.g., \`ng generate component my-comp --project=my-app\`). * Identifying the \`root\` and \`sourceRoot\` of a project to read, analyze, or modify its files. -* Determining if a project is an \`application\` or a \`library\`. +* Determining a project's unit test framework (\`unitTestFramework\`) before writing or modifying tests. * Getting the \`selectorPrefix\` for a project before generating a new component to ensure it follows conventions. * Identifying the major version of the Angular framework for each workspace, which is crucial for monorepos. * Determining a project's primary function by inspecting its builder (e.g., '@angular-devkit/build-angular:browser' for an application). @@ -99,6 +107,10 @@ their types, and their locations. * **Working Directory:** Shell commands for a project (like \`ng generate\`) **MUST** be executed from the parent directory of the \`path\` field for the relevant workspace. +* **Unit Testing:** The \`unitTestFramework\` field tells you which testing API to use (e.g., Jasmine, Jest). + If the value is 'unknown', you **MUST** inspect the project's configuration files + (e.g., \`karma.conf.js\`, \`jest.config.js\`, or the 'test' target in \`angular.json\`) to determine the + framework before generating tests. * **Disambiguation:** A monorepo may contain multiple workspaces (e.g., for different applications or even in output directories). Use the \`path\` of each workspace to understand its context and choose the correct project. `, @@ -230,9 +242,55 @@ async function findAngularCoreVersion( return undefined; } -// Types for the structured output of the helper function. +// Helper types inferred from the Zod schema for structured data processing. type WorkspaceData = z.infer[number]; type ParsingError = z.infer[number]; +type VersioningError = z.infer[number]; + +/** + * Determines the unit test framework for a project based on its 'test' target configuration. + * It handles both the modern `@angular/build:unit-test` builder with its `runner` option + * and older builders where the framework is inferred from the builder name. + * @param testTarget The 'test' target definition from the workspace configuration. + * @returns The name of the unit test framework ('jasmine', 'jest', 'vitest'), 'unknown' if + * the framework can't be determined from a known builder, or `undefined` if there is no test target. + */ +function getUnitTestFramework( + testTarget: import('@angular-devkit/core').workspaces.TargetDefinition | undefined, +): 'jasmine' | 'jest' | 'vitest' | 'unknown' | undefined { + if (!testTarget) { + return undefined; + } + + // For the new unit-test builder, the runner option directly informs the framework. + if (testTarget.builder === '@angular/build:unit-test') { + const runner = testTarget.options?.['runner'] as 'karma' | 'vitest' | undefined; + if (runner === 'karma') { + return 'jasmine'; // Karma is a runner, but the framework is Jasmine. + } else { + return runner; // For 'vitest', the runner and framework are the same. + } + } + + // Fallback for older builders where the framework is inferred from the builder name. + if (testTarget.builder) { + const testBuilder = testTarget.builder; + if ( + testBuilder.includes('karma') || + testBuilder === '@angular-devkit/build-angular:web-test-runner' + ) { + return 'jasmine'; + } else if (testBuilder.includes('jest')) { + return 'jest'; + } else if (testBuilder.includes('vitest')) { + return 'vitest'; + } else { + return 'unknown'; + } + } + + return undefined; +} /** * Loads, parses, and transforms a single angular.json file into the tool's output format. @@ -255,6 +313,8 @@ async function loadAndParseWorkspace( const ws = await AngularWorkspace.load(configFile); const projects = []; for (const [name, project] of ws.projects.entries()) { + const unitTestFramework = getUnitTestFramework(project.targets.get('test')); + projects.push({ name, type: project.extensions['projectType'] as 'application' | 'library' | undefined, @@ -262,6 +322,7 @@ async function loadAndParseWorkspace( root: project.root, sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'), selectorPrefix: project.extensions['prefix'] as string, + unitTestFramework, }); } @@ -278,9 +339,6 @@ async function loadAndParseWorkspace( } } -// Types for the structured output of the helper function. -type VersioningError = z.infer[number]; - /** * Processes a single `angular.json` file to extract workspace and framework version information. * @param configFile The path to the `angular.json` file.