diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts new file mode 100644 index 000000000..6dd0f9461 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/watch/cdk-watch-detects-file-changes-with-glob-patterns.integtest.ts @@ -0,0 +1,110 @@ +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../../lib'; + +jest.setTimeout(10 * 60 * 1000); // 10 minutes for watch tests + +integTest( + 'cdk watch detects file changes with glob patterns', + withDefaultFixture(async (fixture) => { + // Create a test file that will be watched + const testFile = path.join(fixture.integTestDir, 'watch-test-file.ts'); + fs.writeFileSync(testFile, 'export const initial = true;'); + + // Update cdk.json to include watch configuration + const cdkJsonPath = path.join(fixture.integTestDir, 'cdk.json'); + const cdkJson = JSON.parse(fs.readFileSync(cdkJsonPath, 'utf-8')); + cdkJson.watch = { + include: ['**/*.ts', '**/*.js'], + exclude: ['node_modules/**', 'cdk.out/**', '**/*.d.ts'], + }; + fs.writeFileSync(cdkJsonPath, JSON.stringify(cdkJson, null, 2)); + + await fixture.cli.makeCliAvailable(); + + let output = ''; + + // Start cdk watch with detached process group for clean termination + const watchProcess = child_process.spawn('cdk', [ + 'watch', '--hotswap', '-v', fixture.fullStackName('test-1'), + ], { + cwd: fixture.integTestDir, + shell: true, + detached: true, + env: { ...process.env, ...fixture.cdkShellEnv() }, + }); + + watchProcess.stdout?.on('data', (data) => { + output += data.toString(); + fixture.log(data.toString()); + }); + watchProcess.stderr?.on('data', (data) => { + output += data.toString(); + fixture.log(data.toString()); + }); + + try { + await waitForOutput(() => output, "Triggering initial 'cdk deploy'", 120000); + fixture.log('✓ Watch started'); + + await waitForOutput(() => output, 'deployment time', 300000); + fixture.log('✓ Initial deployment completed'); + + // Modify the test file to trigger a watch event + fs.writeFileSync(testFile, 'export const modified = true;'); + + await waitForOutput(() => output, 'Detected change to', 60000); + fixture.log('✓ Watch detected file change'); + + // Wait for second deployment + await waitForCondition( + () => (output.match(/deployment time/g) || []).length >= 2, + 60000, + 'second deployment to complete', + ); + fixture.log('✓ Deployment triggered after file change'); + } finally { + // Kill entire process group + if (watchProcess.pid) { + try { + process.kill(-watchProcess.pid, 'SIGKILL'); + } catch { + /* ignore */ + } + } + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Use separate output dir to avoid conflicts with lingering watch process + await fixture.cdkDestroy('test-1', { options: ['--output', 'cdk-destroy.out'] }); + } + }), +); + +async function waitForOutput(getOutput: () => string, searchString: string, timeoutMs: number): Promise { + const startTime = Date.now(); + return new Promise((resolve, reject) => { + const check = () => { + if (getOutput().includes(searchString)) return resolve(); + if (Date.now() - startTime > timeoutMs) { + return reject(new Error(`Timeout waiting for: "${searchString}"`)); + } + setTimeout(check, 1000); + }; + check(); + }); +} + +async function waitForCondition(condition: () => boolean, timeoutMs: number, description: string): Promise { + const startTime = Date.now(); + return new Promise((resolve, reject) => { + const check = () => { + if (condition()) return resolve(); + if (Date.now() - startTime > timeoutMs) { + return reject(new Error(`Timeout waiting for ${description}`)); + } + setTimeout(check, 1000); + }; + check(); + }); +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/toolkit-lib-integ-tests/toolkit-watch-glob.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/toolkit-lib-integ-tests/toolkit-watch-glob.integtest.ts new file mode 100644 index 000000000..4f78a12b1 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/toolkit-lib-integ-tests/toolkit-watch-glob.integtest.ts @@ -0,0 +1,148 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import * as fs from 'fs'; +import * as path from 'path'; +import * as toolkit from '@aws-cdk/toolkit-lib'; +import { assemblyFromCdkAppDir, toolkitFromFixture } from './toolkit-helpers'; +import { integTest, withDefaultFixture } from '../../lib'; + +/** + * Integration tests for toolkit-lib watch with glob pattern matching. + * + * These tests verify that the chokidar v4 glob pattern fix works correctly + * by using the actual Toolkit.watch() method with real file system operations. + */ + +integTest( + 'toolkit watch detects file changes with glob patterns', + withDefaultFixture(async (fixture) => { + const tk = toolkitFromFixture(fixture); + const assembly = await assemblyFromCdkAppDir(tk, fixture); + + // Track events received from the watcher + const receivedEvents: Array<{ code: string | undefined; message: string }> = []; + + // Create a custom IoHost to capture watch events + const customTk = new toolkit.Toolkit({ + ioHost: { + notify: async (msg) => { + receivedEvents.push({ code: msg.code, message: msg.message }); + }, + requestResponse: async (): Promise => undefined as unknown as T, + }, + }); + + // Create a test file in the watch directory + const testFile = path.join(fixture.integTestDir, 'watch-test-file.ts'); + + // Start watching with specific include patterns + const watcher = await customTk.watch(assembly, { + include: ['**/*.ts'], + exclude: ['**/node_modules/**', '**/*.test.ts'], + watchDir: fixture.integTestDir, + // Use a deployment method that won't actually deploy (we just want to test file watching) + deploymentMethod: { method: 'hotswap' }, + }); + + try { + // Wait a bit for the watcher to initialize + await sleep(1000); + + // Create a new .ts file - this should be detected + fs.writeFileSync(testFile, 'export const watchTest = true;'); + + // Wait for the file change to be detected + await sleep(2000); + + // Verify that the watcher detected the file + const observingEvents = receivedEvents.filter(e => + e.code === 'CDK_TOOLKIT_I5311' || // observing file + e.code === 'CDK_TOOLKIT_I5312' || // detected change + e.code === 'CDK_TOOLKIT_I5314', // triggering deploy + ); + + fixture.log(`Received ${observingEvents.length} watch-related events`); + for (const event of observingEvents) { + fixture.log(` ${event.code}: ${event.message.substring(0, 100)}...`); + } + + // The watcher should have received the ready event and started observing + const hasReadyOrObserving = receivedEvents.some(e => + e.code === 'CDK_TOOLKIT_I5314' || // triggering initial deploy + e.code === 'CDK_TOOLKIT_I5311', // observing files + ); + + if (!hasReadyOrObserving) { + throw new Error('Watcher did not emit ready/observing events'); + } + + fixture.log('✓ Toolkit watch successfully initialized and detected files'); + } finally { + // Clean up - dispose and wait for async operations to settle + await watcher.dispose(); + await sleep(1000); // Allow async operations to complete + if (fs.existsSync(testFile)) { + fs.unlinkSync(testFile); + } + } + }), +); + +integTest( + 'toolkit watch excludes node_modules and dotfiles by default', + withDefaultFixture(async (fixture) => { + const tk = toolkitFromFixture(fixture); + const assembly = await assemblyFromCdkAppDir(tk, fixture); + + // Track the exclude patterns that were configured + const configMessages: string[] = []; + + // Create a custom IoHost to capture configuration messages + const customTk = new toolkit.Toolkit({ + ioHost: { + notify: async (msg) => { + if (msg.code === 'CDK_TOOLKIT_I5310') { + configMessages.push(msg.message); + } + }, + requestResponse: async (): Promise => undefined as unknown as T, + }, + }); + + // Start watching with default exclude patterns + const watcher = await customTk.watch(assembly, { + include: ['**'], + watchDir: fixture.integTestDir, + deploymentMethod: { method: 'hotswap' }, + }); + + try { + // Wait for initialization + await sleep(500); + + // Verify that default excludes are applied + const configMsg = configMessages.find(m => m.includes("'exclude' patterns")); + + if (!configMsg) { + throw new Error('Did not receive exclude patterns configuration message'); + } + + // Check that default excludes are present + if (!configMsg.includes('node_modules')) { + throw new Error('Default excludes should include node_modules'); + } + if (!configMsg.includes('.*')) { + throw new Error('Default excludes should include dotfiles (.*)'); + } + + fixture.log('✓ Toolkit watch applies default exclude patterns correctly'); + fixture.log(` Config: ${configMsg.substring(0, 200)}...`); + } finally { + await watcher.dispose(); + await sleep(1000); // Allow async operations to complete + } + }), +); + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +}