Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: including the .js files is a bad idea for some use cases (say you're also auto-compiling ts changes, now you're double counting). doesn't seem necessary in your test

exclude: ['node_modules/**', 'cdk.out/**', '**/*.d.ts'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this part isn't tested. in this test or a new one, we should be testing that changes to exclude files do not trigger deployment.

};
fs.writeFileSync(cdkJsonPath, JSON.stringify(cdkJson, null, 2));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can also just call touch on the file.


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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how did you come up with these timeout durations?

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'] });
Comment on lines +78 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to manually delete. The rest harness is doing this for us.

Suggested change
// 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<void> {
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<void> {
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();
});
}
Comment on lines +84 to +110
Copy link
Contributor

@mrgrain mrgrain Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there really no existing helpers for this?

Original file line number Diff line number Diff line change
@@ -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 <T>(): Promise<T> => 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one test per file. Pls move to separate file.

'toolkit watch excludes node_modules and dotfiles by default',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this test not part of the cli integ tests also?

Copy link
Contributor Author

@iankhou iankhou Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it will add more time to the test, while offering limited utility IMO. Probably another minute or so. See my PR description where I addressed this discrepancy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont understand why this would be the case. i think you need to
a) touch node_modules / dotfiles
b) assert that we do not see Detected change to '<file>' -- this happens nearly instantly if the file is being watched, so i dont see why it would take another minute.

as for whether this offers utility, i think its worse if we accidentally don't exclude files from watch than if we accidentally don't include files.

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 <T>(): Promise<T> => 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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Loading