Skip to content
Merged
250 changes: 248 additions & 2 deletions e2e/nx/src/configure-ai-agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ import {
} from '@nx/e2e-utils';

describe('configure-ai-agents', () => {
beforeAll(() => newProject({ packages: ['@nx/web', '@nx/js', '@nx/react'] }));
afterAll(() => cleanupProject());
beforeAll(() => {
process.env.NX_USE_LOCAL = 'true';
newProject({ packages: ['@nx/web', '@nx/js', '@nx/react'] });
});
afterAll(() => {
delete process.env.NX_USE_LOCAL;
cleanupProject();
});

it('should generate local configuration', () => {
runCLI(`configure-ai-agents --agents claude --no-interactive`);
Expand Down Expand Up @@ -184,4 +190,244 @@ describe('configure-ai-agents', () => {
expect(skillsContents.length).toBeGreaterThan(0);
});
});

describe('--no-interactive without --agents (AI agent-like mode)', () => {
// Without explicit --agents, --no-interactive should behave like AI agent
// mode: update outdated agents only, report non-configured ones.

beforeAll(() => {
// Start clean: remove all agent configs
removeFile('CLAUDE.md');
removeFile('.claude');
removeFile('.gemini');
removeFile('AGENTS.md');
removeFile('opencode.json');
removeFile('.opencode');
removeFile('.codex');
});

it('should report all agents as not yet configured on a clean workspace', () => {
const output = runCLI('configure-ai-agents --no-interactive');

// Nothing should have been configured
expect(() => readFile('CLAUDE.md')).toThrow();

// Should report non-configured agents
expect(output).toContain('not yet configured');
expect(output).toContain('configure-ai-agents --agents');
});

it('should update outdated agents and report non-configured ones', () => {
// Configure claude first
runCLI('configure-ai-agents --agents claude --no-interactive');
expect(readFile('CLAUDE.md')).toContain('# General Guidelines');

// Make it outdated
updateFile('CLAUDE.md', (content: string) =>
content.replace('nx_docs', 'nx_docs_outdated')
);

const output = runCLI('configure-ai-agents --no-interactive');

// Should have updated the outdated claude config
expect(readFile('CLAUDE.md')).not.toContain('nx_docs_outdated');
expect(output).toContain('configured successfully');

// Should report non-configured agents
expect(output).toContain('not yet configured');
});

it('should report up-to-date when all configured agents are current', () => {
// Claude is already fully configured from previous test
const output = runCLI('configure-ai-agents --no-interactive');

expect(output).toContain('up to date');
});

it('should not configure partially configured agents', () => {
// Remove the rules file to make claude partially configured
removeFile('CLAUDE.md');

const output = runCLI('configure-ai-agents --no-interactive');

// Should NOT have restored the missing rules file — partial agents
// are not auto-configured (same as AI agent mode for non-detected agents)
expect(() => readFile('CLAUDE.md')).toThrow();
expect(output).toContain('up to date');

// Restore for subsequent tests
runCLI('configure-ai-agents --agents claude --no-interactive');
});
});

describe('when running from a detected AI agent', () => {
// Simulate Claude Code detection via CLAUDECODE env var.
// No --no-interactive flag, no --agents flag: the command auto-detects
// which agent is calling it and configures accordingly.
const claudeEnv = { CLAUDECODE: '1' };

beforeAll(() => {
// Start clean: remove all agent configs
removeFile('CLAUDE.md');
removeFile('.claude');
removeFile('.gemini');
});

it('should configure the detected agent without prompting on a clean workspace', () => {
// No --agents flag — the command detects claude via CLAUDECODE env var
const output = runCLI('configure-ai-agents', {
env: claudeEnv,
});

// Should have configured claude (the detected agent)
expect(readFile('CLAUDE.md')).toContain('# General Guidelines');
expect(readFile('.claude/settings.json')).toContain('nx-claude-plugins');

// Should mention other unconfigured agents with a command to configure them
expect(output).toContain('not yet configured');
expect(output).toContain('configure-ai-agents --agents');
});

it('should report up-to-date when the detected agent is fully configured', () => {
// Claude is already fully configured from previous test
const output = runCLI('configure-ai-agents', {
env: claudeEnv,
});

expect(output).toContain('up to date');
});

it('should update the detected agent when its rules are outdated', () => {
// Make claude outdated
updateFile('CLAUDE.md', (content: string) =>
content.replace('nx_docs', 'nx_docs_outdated')
);

const output = runCLI('configure-ai-agents', {
env: claudeEnv,
});

// Should have updated claude's rules
expect(readFile('CLAUDE.md')).not.toContain('nx_docs_outdated');
expect(output).toContain('configured successfully');
});

it('should update other outdated agents alongside the detected agent', () => {
// Configure gemini fully first
runCLI('configure-ai-agents --agents gemini --no-interactive');
expect(readFile('AGENTS.md')).toBeTruthy();

// Make gemini outdated
updateFile('AGENTS.md', (content: string) =>
content.replace('nx_docs', 'nx_docs_outdated')
);

// Run from detected claude agent — should also update outdated gemini
const output = runCLI('configure-ai-agents', {
env: claudeEnv,
});

// Gemini should have been updated because it was outdated
expect(readFile('AGENTS.md')).not.toContain('nx_docs_outdated');
expect(output).toContain('configured successfully');
});

it('should list non-configured agents with a suggested command', () => {
// Remove gemini config to make it non-configured
// Note: .gemini removal is sufficient — without settings.json, gemini is non-configured
removeFile('.gemini');

const output = runCLI('configure-ai-agents', {
env: claudeEnv,
});

// Claude is up-to-date, gemini is non-configured
// Should suggest running configure-ai-agents for gemini
expect(output).toContain('not yet configured');
expect(output).toContain('gemini');
expect(output).toContain('configure-ai-agents --agents');
});

it('should ignore the detected agent when --agents is explicitly passed', () => {
// Clean up both agents
removeFile('CLAUDE.md');
removeFile('.claude');
removeFile('.gemini');

// Explicitly pass --agents gemini — should ONLY configure gemini,
// ignoring the detected claude agent entirely
runCLI('configure-ai-agents --agents gemini --no-interactive', {
env: claudeEnv,
});

// Gemini should be configured because it was explicitly requested
expect(readFile('AGENTS.md')).toContain('# General Guidelines');

// Claude should NOT be configured — --agents overrides detection
expect(() => readFile('CLAUDE.md')).toThrow();
});

it('should throw with --check when the detected agent is outdated', () => {
// Restore claude config (previous test removed it)
runCLI('configure-ai-agents --agents claude --no-interactive');

// Make claude outdated
updateFile('CLAUDE.md', (content: string) =>
content.replace('nx_docs', 'nx_docs_outdated')
);

let didThrow = false;
try {
runCLI('configure-ai-agents --check', {
env: claudeEnv,
});
} catch {
didThrow = true;
}
expect(didThrow).toBe(true);

// Restore
runCLI('configure-ai-agents --agents claude --no-interactive');
});

it('should throw with --check when a non-detected agent is outdated', () => {
// Configure gemini fully
runCLI('configure-ai-agents --agents gemini --no-interactive');

// Make gemini outdated while claude stays up-to-date
updateFile('AGENTS.md', (content: string) =>
content.replace('nx_docs', 'nx_docs_outdated')
);

let didThrow = false;
try {
runCLI('configure-ai-agents --check', {
env: claudeEnv,
});
} catch {
didThrow = true;
}
expect(didThrow).toBe(true);

// Restore
runCLI('configure-ai-agents --agents gemini --no-interactive');
});

it('should not throw with --check when --agents scopes to up-to-date agents only', () => {
// Make claude outdated
updateFile('CLAUDE.md', (content: string) =>
content.replace('nx_docs', 'nx_docs_outdated')
);

// --agents gemini --check should only check gemini, ignoring
// the detected (and outdated) claude entirely
const output = runCLI('configure-ai-agents --agents gemini --check', {
env: claudeEnv,
});
expect(output).toContain('up to date');

// Restore
runCLI('configure-ai-agents --agents claude --no-interactive');
});
});
});
17 changes: 17 additions & 0 deletions e2e/utils/get-env-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export function getStrippedEnvironmentVariables() {
'NX_ISOLATE_PLUGINS',
'NX_VERBOSE_LOGGING',
'NX_NATIVE_LOGGING',
'NX_USE_LOCAL',
];

if (key.startsWith('NX_') && !allowedKeys.includes(key)) {
Expand All @@ -169,6 +170,22 @@ export function getStrippedEnvironmentVariables() {
return false;
}

// Remove AI agent detection env vars to prevent the test runner's
// environment (e.g., running inside Claude Code) from leaking into
// e2e test subprocesses. Tests that need these pass them explicitly.
const aiAgentEnvVars = [
'CLAUDECODE',
'CLAUDE_CODE',
'OPENCODE',
'GEMINI_CLI',
'CURSOR_TRACE_ID',
'COMPOSER_NO_INTERACTION',
'REPL_ID',
];
if (aiAgentEnvVars.includes(key)) {
return false;
}

return true;
})
);
Expand Down
Loading
Loading