diff --git a/packages/angular/cli/src/commands/mcp/tools/test.ts b/packages/angular/cli/src/commands/mcp/tools/test.ts index 2ace1496dd46..9c05a2f0f29b 100644 --- a/packages/angular/cli/src/commands/mcp/tools/test.ts +++ b/packages/angular/cli/src/commands/mcp/tools/test.ts @@ -29,8 +29,16 @@ const testToolOutputSchema = z.object({ export type TestToolOutput = z.infer; +function shouldUseHeadlessOption( + testTarget: import('@angular-devkit/core').workspaces.TargetDefinition | undefined, +): boolean { + return ( + testTarget?.builder === '@angular/build:unit-test' && testTarget.options?.['runner'] !== 'karma' + ); +} + export async function runTest(input: TestToolInput, context: McpToolContext) { - const { workspacePath, projectName } = await resolveWorkspaceAndProject({ + const { workspace, workspacePath, projectName } = await resolveWorkspaceAndProject({ host: context.host, workspacePathInput: input.workspace, projectNameInput: input.project, @@ -40,8 +48,13 @@ export async function runTest(input: TestToolInput, context: McpToolContext) { // Build "ng"'s command line. const args = ['test', projectName]; - // This is ran by the agent so we want a non-watched, headless test. - args.push('--browsers', 'ChromeHeadless'); + if (shouldUseHeadlessOption(workspace.projects.get(projectName)?.targets.get('test'))) { + args.push('--headless', 'true'); + } else { + // Karma-based projects need an explicit headless browser for non-interactive MCP execution. + args.push('--browsers', 'ChromeHeadless'); + } + args.push('--watch', 'false'); if (input.filter) { @@ -83,7 +96,8 @@ Perform a one-off, non-watched unit test execution with ng test. * This tool uses "ng test". * It supports filtering by spec name if the underlying builder supports it (e.g., 'unit-test' builder). -* This runs a headless Chrome as a browser, so requires Chrome to be installed. +* For the "@angular/build:unit-test" builder with Vitest, this tool requests headless execution via "--headless true". +* For Karma-based projects, this tool forces headless Chrome with "--browsers ChromeHeadless", so Chrome must be installed. `, isReadOnly: false, diff --git a/packages/angular/cli/src/commands/mcp/tools/test_spec.ts b/packages/angular/cli/src/commands/mcp/tools/test_spec.ts index 722432bfed6c..487c986cdcd2 100644 --- a/packages/angular/cli/src/commands/mcp/tools/test_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/test_spec.ts @@ -93,4 +93,40 @@ describe('Test Tool', () => { expect(structuredContent.status).toBe('failure'); expect(structuredContent.logs).toEqual([...testLogs, 'Test failed']); }); + + it('should use the headless option for the unit-test builder when using Vitest', async () => { + addProjectToWorkspace(mockContext.workspace.projects, 'my-vitest-app', { + test: { + builder: '@angular/build:unit-test', + options: { + runner: 'vitest', + }, + }, + }); + + await runTest({ project: 'my-vitest-app' }, mockContext); + + expect(mockHost.runCommand).toHaveBeenCalledWith( + 'ng', + ['test', 'my-vitest-app', '--headless', 'true', '--watch', 'false'], + { cwd: '/test' }, + ); + }); + + it('should use the headless option for the unit-test builder when the runner is omitted', async () => { + addProjectToWorkspace(mockContext.workspace.projects, 'my-default-vitest-app', { + test: { + builder: '@angular/build:unit-test', + options: {}, + }, + }); + + await runTest({ project: 'my-default-vitest-app' }, mockContext); + + expect(mockHost.runCommand).toHaveBeenCalledWith( + 'ng', + ['test', 'my-default-vitest-app', '--headless', 'true', '--watch', 'false'], + { cwd: '/test' }, + ); + }); });