From 7b440f58f00a0383a3efa7323ccd7a754595c1cf Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:36:22 -0400 Subject: [PATCH 1/3] test(cli): snapshot the cdk synth output contract Adds IoHostRecorder NDJSON snapshots exercising every `cdk synth` CLI path, generated against the current implementation, so any change to synth's user-visible output shows up as a committed snapshot diff (the protection list/validate/destroy/deploy/metadata/refactor already have). 16 scenarios: single-stack YAML/JSON template printing with Rules.CheckBootstrapVersion obscuring, --quiet, multi-stack and nested-assembly output, stack ids containing glob metacharacters, the CI-mode flags-message gating that keeps stdout valid YAML, stage-only (pipeline) apps with an empty selection, non-exclusive upstream dependency expansion, validation failures (explicit selection, validateOnSynth, --no-validation), --strict on warning annotations, --ignore-errors, and the unmatched-pattern error. Also teaches the recorder's default scrubbers about random mkdtemp assembly dirs (cdk.outXXXXXX), which the multi-stack success line embeds. --- packages/aws-cdk/test/_helpers/io-recorder.ts | 2 + ...ror_annotations_on_a_selected_stack.ndjson | 4 + ...ls_synthesis_on_warning_annotations.ndjson | 3 + ...s_warning_to_keep_stdout_valid_YAML.ndjson | 3 + ...th_--quiet_allows_the_flags_warning.ndjson | 3 + ...elects_no_stacks_and_still_succeeds.ndjson | 5 + ...s_fails_synth_when_validation_is_on.ndjson | 3 + ...s_is_tolerated_with_--no-validation.ndjson | 5 + ...s_with_the_historical_error_message.ndjson | 2 + ...h_error_annotations_fails_synthesis.ndjson | 3 + ...success_and_supply-a-stack-id_lines.ndjson | 5 + ..._hierarchical_id_in_the_supply_line.ndjson | 5 + ...on_expands_to_upstream_dependencies.ndjson | 6 + ...YAML_template_and_the_flags_warning.ndjson | 4 + ...ith_--json_prints_the_JSON_template.ndjson | 4 + ...emplate_but_keeps_the_flags_warning.ndjson | 3 + ...are_handed_to_toolkit-lib_literally.ndjson | 4 + packages/aws-cdk/test/commands/synth.test.ts | 233 ++++++++++++++++++ 18 files changed, 297 insertions(+) create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_--ignore-errors_tolerates_error_annotations_on_a_selected_stack.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_--strict_fails_synthesis_on_warning_annotations.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_CI_mode_single_stack_skips_the_flags_warning_to_keep_stdout_valid_YAML.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_CI_mode_single_stack_with_--quiet_allows_the_flags_warning.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_stage-only_app_selects_no_stacks_and_still_succeeds.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_validateOnSynth_stack_with_errors_fails_synth_when_validation_is_on.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_validateOnSynth_stack_with_errors_is_tolerated_with_--no-validation.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_an_unmatched_pattern_fails_with_the_historical_error_message.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_explicitly_selected_stack_with_error_annotations_fails_synthesis.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_multiple_stacks_print_the_success_and_supply-a-stack-id_lines.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_nested-assembly_stacks_are_addressed_by_hierarchical_id_in_the_supply_line.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_non-exclusive_selection_expands_to_upstream_dependencies.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_prints_the_obscured_YAML_template_and_the_flags_warning.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_with_--json_prints_the_JSON_template.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_with_--quiet_prints_no_template_but_keeps_the_flags_warning.ndjson create mode 100644 packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_stack_ids_containing_glob_metacharacters_are_handed_to_toolkit-lib_literally.ndjson create mode 100644 packages/aws-cdk/test/commands/synth.test.ts diff --git a/packages/aws-cdk/test/_helpers/io-recorder.ts b/packages/aws-cdk/test/_helpers/io-recorder.ts index 99b892207..05a547fba 100644 --- a/packages/aws-cdk/test/_helpers/io-recorder.ts +++ b/packages/aws-cdk/test/_helpers/io-recorder.ts @@ -38,6 +38,8 @@ function defaultScrubbers(): Scrubber[] { { pattern: /\n\s+at\s+[^\n]*/g, replacement: '' }, // The OS temp dir (tests chdir into a temp dir) { pattern: new RegExp(escapeRegExp(fs.realpathSync(os.tmpdir())), 'g'), replacement: '' }, + // The random suffix of mkdtemp-created assembly output dirs, e.g. "cdk.outAb12Cd" + { pattern: /cdk\.out[a-zA-Z0-9]{6}/g, replacement: 'cdk.out' }, ]; } diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_--ignore-errors_tolerates_error_annotations_on_a_selected_stack.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_--ignore-errors_tolerates_error_annotations_on_a_selected_stack.ndjson new file mode 100644 index 000000000..9fb6c9feb --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_--ignore-errors_tolerates_error_annotations_on_a_selected_stack.ndjson @@ -0,0 +1,4 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"error","code":"CDK_TOOLKIT_E9600","message":"ERROR this is an error (Construct Annotations)\n resource\n Rule Construct-Annotations::error-annotation"} +{"seq":3,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_--strict_fails_synthesis_on_warning_annotations.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_--strict_fails_synthesis_on_warning_annotations.ndjson new file mode 100644 index 000000000..87be57808 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_--strict_fails_synthesis_on_warning_annotations.ndjson @@ -0,0 +1,3 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"error","code":"CDK_TOOLKIT_E9600","message":"WARNING this is a warning (Construct Annotations)\n resource\n Acknowledge with 'Construct-Annotations::warning-annotation'"} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_CI_mode_single_stack_skips_the_flags_warning_to_keep_stdout_valid_YAML.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_CI_mode_single_stack_skips_the_flags_warning_to_keep_stdout_valid_YAML.ndjson new file mode 100644 index 000000000..a73a1a2c2 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_CI_mode_single_stack_skips_the_flags_warning_to_keep_stdout_valid_YAML.ndjson @@ -0,0 +1,3 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"result","code":null,"message":"Resources:\n TemplateName: Test-Stack-A\n"} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_CI_mode_single_stack_with_--quiet_allows_the_flags_warning.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_CI_mode_single_stack_with_--quiet_allows_the_flags_warning.ndjson new file mode 100644 index 000000000..35b5c9f3f --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_CI_mode_single_stack_with_--quiet_allows_the_flags_warning.ndjson @@ -0,0 +1,3 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_stage-only_app_selects_no_stacks_and_still_succeeds.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_stage-only_app_selects_no_stacks_and_still_succeeds.ndjson new file mode 100644 index 000000000..4ef188131 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_stage-only_app_selects_no_stacks_and_still_succeeds.ndjson @@ -0,0 +1,5 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"info","code":null,"message":"Successfully synthesized to /cdk.out"} +{"seq":3,"type":"notify","action":"synth","level":"info","code":null,"message":"Supply a stack id () to display its template."} +{"seq":4,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_validateOnSynth_stack_with_errors_fails_synth_when_validation_is_on.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_validateOnSynth_stack_with_errors_fails_synth_when_validation_is_on.ndjson new file mode 100644 index 000000000..7d133eea3 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_validateOnSynth_stack_with_errors_fails_synth_when_validation_is_on.ndjson @@ -0,0 +1,3 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"error","code":"CDK_TOOLKIT_E9600","message":"ERROR this is an error (Construct Annotations)\n resource\n Rule Construct-Annotations::error-annotation"} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_validateOnSynth_stack_with_errors_is_tolerated_with_--no-validation.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_validateOnSynth_stack_with_errors_is_tolerated_with_--no-validation.ndjson new file mode 100644 index 000000000..a45d09f1b --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_a_validateOnSynth_stack_with_errors_is_tolerated_with_--no-validation.ndjson @@ -0,0 +1,5 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"info","code":null,"message":"Successfully synthesized to /cdk.out"} +{"seq":3,"type":"notify","action":"synth","level":"info","code":null,"message":"Supply a stack id (Test-Stack-A-Display-Name, Test-Stack-B) to display its template."} +{"seq":4,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_an_unmatched_pattern_fails_with_the_historical_error_message.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_an_unmatched_pattern_fails_with_the_historical_error_message.ndjson new file mode 100644 index 000000000..414d10c8c --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_an_unmatched_pattern_fails_with_the_historical_error_message.ndjson @@ -0,0 +1,2 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_explicitly_selected_stack_with_error_annotations_fails_synthesis.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_explicitly_selected_stack_with_error_annotations_fails_synthesis.ndjson new file mode 100644 index 000000000..7d133eea3 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_explicitly_selected_stack_with_error_annotations_fails_synthesis.ndjson @@ -0,0 +1,3 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"error","code":"CDK_TOOLKIT_E9600","message":"ERROR this is an error (Construct Annotations)\n resource\n Rule Construct-Annotations::error-annotation"} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_multiple_stacks_print_the_success_and_supply-a-stack-id_lines.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_multiple_stacks_print_the_success_and_supply-a-stack-id_lines.ndjson new file mode 100644 index 000000000..a45d09f1b --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_multiple_stacks_print_the_success_and_supply-a-stack-id_lines.ndjson @@ -0,0 +1,5 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"info","code":null,"message":"Successfully synthesized to /cdk.out"} +{"seq":3,"type":"notify","action":"synth","level":"info","code":null,"message":"Supply a stack id (Test-Stack-A-Display-Name, Test-Stack-B) to display its template."} +{"seq":4,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_nested-assembly_stacks_are_addressed_by_hierarchical_id_in_the_supply_line.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_nested-assembly_stacks_are_addressed_by_hierarchical_id_in_the_supply_line.ndjson new file mode 100644 index 000000000..f27f61719 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_nested-assembly_stacks_are_addressed_by_hierarchical_id_in_the_supply_line.ndjson @@ -0,0 +1,5 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"info","code":null,"message":"Successfully synthesized to /cdk.out"} +{"seq":3,"type":"notify","action":"synth","level":"info","code":null,"message":"Supply a stack id (Test-Stack-A-Display-Name, Test-Stack-A/nested) to display its template."} +{"seq":4,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_non-exclusive_selection_expands_to_upstream_dependencies.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_non-exclusive_selection_expands_to_upstream_dependencies.ndjson new file mode 100644 index 000000000..ddecad314 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_non-exclusive_selection_expands_to_upstream_dependencies.ndjson @@ -0,0 +1,6 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"info","code":"CDK_TOOLKIT_I1002","message":"Including dependency stacks: Test-Stack-B"} +{"seq":3,"type":"notify","action":"synth","level":"info","code":null,"message":"Successfully synthesized to /cdk.out"} +{"seq":4,"type":"notify","action":"synth","level":"info","code":null,"message":"Supply a stack id (Test-Stack-B, Test-Stack-D) to display its template."} +{"seq":5,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_prints_the_obscured_YAML_template_and_the_flags_warning.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_prints_the_obscured_YAML_template_and_the_flags_warning.ndjson new file mode 100644 index 000000000..3435f6c64 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_prints_the_obscured_YAML_template_and_the_flags_warning.ndjson @@ -0,0 +1,4 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"result","code":null,"message":"Resources:\n TemplateName: Test-Stack-A\n"} +{"seq":3,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_with_--json_prints_the_JSON_template.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_with_--json_prints_the_JSON_template.ndjson new file mode 100644 index 000000000..78e2339d4 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_with_--json_prints_the_JSON_template.ndjson @@ -0,0 +1,4 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"result","code":null,"message":"{\n \"Resources\": {\n \"TemplateName\": \"Test-Stack-A\"\n }\n}"} +{"seq":3,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_with_--quiet_prints_no_template_but_keeps_the_flags_warning.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_with_--quiet_prints_no_template_but_keeps_the_flags_warning.ndjson new file mode 100644 index 000000000..35b5c9f3f --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_single_stack_with_--quiet_prints_no_template_but_keeps_the_flags_warning.ndjson @@ -0,0 +1,3 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_stack_ids_containing_glob_metacharacters_are_handed_to_toolkit-lib_literally.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_stack_ids_containing_glob_metacharacters_are_handed_to_toolkit-lib_literally.ndjson new file mode 100644 index 000000000..bfec4f8ce --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/synth/cdk_synth_stack_ids_containing_glob_metacharacters_are_handed_to_toolkit-lib_literally.ndjson @@ -0,0 +1,4 @@ +{"seq":0,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"synth","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":2,"type":"notify","action":"synth","level":"result","code":null,"message":"Resources:\n TemplateName: Data[prod]\n"} +{"seq":3,"type":"notify","action":"synth","level":"warn","code":null,"message":"1 feature flags are not configured. Run 'cdk flags --unstable=flags' to learn more."} diff --git a/packages/aws-cdk/test/commands/synth.test.ts b/packages/aws-cdk/test/commands/synth.test.ts new file mode 100644 index 000000000..c7354207c --- /dev/null +++ b/packages/aws-cdk/test/commands/synth.test.ts @@ -0,0 +1,233 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import type { FeatureFlag } from '@aws-cdk/toolkit-lib'; +import { Toolkit } from '@aws-cdk/toolkit-lib'; +import { Deployments } from '../../lib/api/deployments'; +import { CdkToolkit } from '../../lib/cli/cdk-toolkit'; +import { CliIoHost } from '../../lib/cli/io-host'; +import { instanceMockFrom, MockCloudExecutable } from '../_helpers'; +import type { TestAssembly } from '../_helpers'; +import { IoHostRecorder } from '../_helpers/io-recorder'; + +// `cdk synth` prints the template of a single selected stack to stdout (or a +// success message for several stacks) and appends a feature-flags warning with +// CI/quiet-dependent gating. Its stdout is a scripting surface (must stay valid +// YAML in CI mode), so these tests run the command end-to-end and snapshot +// everything the user sees (via IoHostRecorder) — the committed NDJSON is the +// assertion for selection, template rendering, message levels and gating. +describe('cdk synth', () => { + const ioHost = CliIoHost.instance(); + let recorder: IoHostRecorder; + + // A flag that `FlagOperations.filterNeedsAttention` keeps (unconfigured, not + // obsolete, unconfigured behavior differs from the recommended value), so + // `displayFlagsMessage` deterministically emits its warning when not gated. + const UNCONFIGURED_FLAG: FeatureFlag = { + module: 'aws-cdk-lib', + name: '@aws-cdk/testing:synthSnapshotFlag', + recommendedValue: true, + unconfiguredBehavesLike: { v2: false }, + }; + + beforeEach(() => { + jest.resetAllMocks(); + ioHost.isCI = false; + ioHost.currentAction = 'synth'; + jest.spyOn(Toolkit.prototype, 'flags').mockResolvedValue([UNCONFIGURED_FLAG]); + // The recorder observes the host (it does not spy on `notify`), so the real + // notify path — and any output listeners — run without any pass-through. + recorder = IoHostRecorder.create(ioHost); + }); + + afterEach(() => { + ioHost.isCI = false; + recorder.matchSnapshot(); + }); + + async function synth( + assembly: TestAssembly, + stackNames: string[], + options: { + exclusively?: boolean; + quiet?: boolean; + autoValidate?: boolean; + json?: boolean; + strict?: boolean; + ignoreErrors?: boolean; + } = {}, + ) { + const cloudExecutable = await MockCloudExecutable.create(assembly, undefined, ioHost, 'synth'); + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: instanceMockFrom(Deployments), + strict: options.strict, + ignoreErrors: options.ignoreErrors, + }); + await toolkit.synth( + stackNames, + options.exclusively ?? true, + options.quiet ?? false, + options.autoValidate, + options.json, + ); + } + + const STACK_A = { + stackName: 'Test-Stack-A', + displayName: 'Test-Stack-A-Display-Name', + // Rules.CheckBootstrapVersion must be obscured from the printed template + template: { + Resources: { TemplateName: 'Test-Stack-A' }, + Rules: { CheckBootstrapVersion: { Assertions: [] } }, + }, + env: 'aws://123456789012/bermuda-triangle-1', + }; + + const STACK_B = { + stackName: 'Test-Stack-B', + template: { Resources: { TemplateName: 'Test-Stack-B' } }, + env: 'aws://123456789012/bermuda-triangle-1', + }; + + const STACK_WITH_ERROR = { + stackName: 'witherrors', + displayName: 'Test-Stack-A/witherrors', + env: 'aws://123456789012/bermuda-triangle-1', + template: { resource: 'errorresource' }, + metadata: { + '/resource': [ + { + type: cxschema.ArtifactMetadataEntryType.ERROR, + data: 'this is an error', + }, + ], + }, + }; + + test('single stack prints the obscured YAML template and the flags warning', async () => { + await synth({ stacks: [STACK_A] }, ['Test-Stack-A-Display-Name']); + }); + + test('single stack with --json prints the JSON template', async () => { + await synth({ stacks: [STACK_A] }, ['Test-Stack-A-Display-Name'], { json: true }); + }); + + test('single stack with --quiet prints no template but keeps the flags warning', async () => { + await synth({ stacks: [STACK_A] }, ['Test-Stack-A-Display-Name'], { quiet: true }); + }); + + test('multiple stacks print the success and supply-a-stack-id lines', async () => { + await synth({ stacks: [STACK_A, STACK_B] }, []); + }); + + test('nested-assembly stacks are addressed by hierarchical id in the supply line', async () => { + // Select the nested stack explicitly (the no-pattern default only selects + // top-level stacks), so the snapshot pins the hierarchical-id handoff. + await synth({ + stacks: [STACK_A, STACK_B], + nestedAssemblies: [{ stacks: [{ ...STACK_WITH_ERROR, metadata: {}, stackName: 'nested', displayName: 'Test-Stack-A/nested' }] }], + }, ['Test-Stack-A-Display-Name', 'Test-Stack-A/nested']); + }); + + test('stack ids containing glob metacharacters are handed to toolkit-lib literally', async () => { + // 'Data[prod]' glob-matches 'Datap' when re-interpreted as a pattern, so an + // unescaped id handoff would select both stacks and print the supply line + // instead of the template. The snapshot must show Data[prod]'s template. + await synth({ + stacks: [ + { + stackName: 'DataProd', + displayName: 'Data[prod]', + template: { Resources: { TemplateName: 'Data[prod]' } }, + env: 'aws://123456789012/bermuda-triangle-1', + }, + { + stackName: 'Datap', + template: { Resources: { TemplateName: 'Datap' } }, + env: 'aws://123456789012/bermuda-triangle-1', + }, + ], + }, ['Data\\[prod\\]']); + }); + + test('CI mode single stack skips the flags warning to keep stdout valid YAML', async () => { + ioHost.isCI = true; + await synth({ stacks: [STACK_A] }, ['Test-Stack-A-Display-Name']); + }); + + test('CI mode single stack with --quiet allows the flags warning', async () => { + ioHost.isCI = true; + await synth({ stacks: [STACK_A] }, ['Test-Stack-A-Display-Name'], { quiet: true }); + }); + + test('explicitly selected stack with error annotations fails synthesis', async () => { + await expect(synth({ + stacks: [STACK_A, STACK_B], + nestedAssemblies: [{ stacks: [{ properties: { validateOnSynth: false }, ...STACK_WITH_ERROR }] }], + }, ['Test-Stack-A/witherrors'], { quiet: true })).rejects.toThrow(/Synthesis finished with errors/); + }); + + test('a validateOnSynth stack with errors fails synth when validation is on', async () => { + await expect(synth({ + stacks: [STACK_A, STACK_B], + nestedAssemblies: [{ stacks: [{ properties: { validateOnSynth: true }, ...STACK_WITH_ERROR }] }], + }, [], { quiet: true, autoValidate: true })).rejects.toThrow(/Synthesis finished with errors/); + }); + + test('a validateOnSynth stack with errors is tolerated with --no-validation', async () => { + await synth({ + stacks: [STACK_A, STACK_B], + nestedAssemblies: [{ stacks: [{ properties: { validateOnSynth: true }, ...STACK_WITH_ERROR }] }], + }, [], { quiet: true, autoValidate: false }); + }); + + test('an unmatched pattern fails with the historical error message', async () => { + await expect(synth({ stacks: [STACK_A] }, ['NoSuchStack'])) + .rejects.toThrow(/No stacks match the name\(s\) NoSuchStack/); + }); + + test('a stage-only app selects no stacks and still succeeds', async () => { + // Pipeline-style apps have no top-level stacks; the no-pattern MainAssembly + // default selects nothing and synth must not fail. + await synth({ + stacks: [], + nestedAssemblies: [{ stacks: [{ ...STACK_B, stackName: 'staged', displayName: 'MyStage/staged' }] }], + }, []); + }); + + test('non-exclusive selection expands to upstream dependencies', async () => { + // Selecting the dependent stack without --exclusively pulls in its + // dependency, taking the multi-stack path. + await synth({ + stacks: [ + STACK_B, + { + stackName: 'Test-Stack-D', + template: { Resources: { TemplateName: 'Test-Stack-D' } }, + env: 'aws://123456789012/bermuda-triangle-1', + depends: ['Test-Stack-B'], + }, + ], + }, ['Test-Stack-D'], { exclusively: false }); + }); + + test('--strict fails synthesis on warning annotations', async () => { + await expect(synth({ + stacks: [{ + ...STACK_B, + metadata: { + '/resource': [{ type: cxschema.ArtifactMetadataEntryType.WARN, data: 'this is a warning' }], + }, + }], + }, ['Test-Stack-B'], { quiet: true, strict: true })).rejects.toThrow(/Synthesis finished with warnings/); + }); + + test('--ignore-errors tolerates error annotations on a selected stack', async () => { + await synth({ + stacks: [STACK_A, STACK_B], + nestedAssemblies: [{ stacks: [{ properties: { validateOnSynth: false }, ...STACK_WITH_ERROR }] }], + }, ['Test-Stack-A/witherrors'], { quiet: true, ignoreErrors: true }); + }); +}); From 4229844e6bf260cdbbb90afa20b0d81c9f3e5aab Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:03:21 -0400 Subject: [PATCH 2/3] test(cli): harden the synth snapshot suite per review - Anchor the cdk.out scrubber on the path separator so it can only match mkdtemp assembly dirs, never prose that happens to start with "cdk.out" (it lives in the shared defaults, so over-matching would affect every suite). - Spread the error-stack fixture before the validateOnSynth override so a future `properties` key on the fixture cannot silently discard the load-bearing override; drop the override entirely where it was dead configuration (tests that never pass autoValidate). - Give the nested-assembly test its own small fixture instead of reusing the error fixture with its errors blanked out. - Correct the header comment: the suite enters at CdkToolkit.synth, so the cli.ts argument mapping (settings-file quiet, --exclusively branch) is not pinned here; cross-reference the spy-style describe('synth') block. - Note that the stage-only snapshot deliberately pins the empty-parens "Supply a stack id ()" output as a baseline, not an endorsement. - Drop the redundant afterEach isCI reset. No snapshot changes: all 16 scenarios still match the committed baselines byte-for-byte. --- packages/aws-cdk/test/_helpers/io-recorder.ts | 8 +++-- packages/aws-cdk/test/commands/synth.test.ts | 30 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/aws-cdk/test/_helpers/io-recorder.ts b/packages/aws-cdk/test/_helpers/io-recorder.ts index 05a547fba..de462f332 100644 --- a/packages/aws-cdk/test/_helpers/io-recorder.ts +++ b/packages/aws-cdk/test/_helpers/io-recorder.ts @@ -38,8 +38,12 @@ function defaultScrubbers(): Scrubber[] { { pattern: /\n\s+at\s+[^\n]*/g, replacement: '' }, // The OS temp dir (tests chdir into a temp dir) { pattern: new RegExp(escapeRegExp(fs.realpathSync(os.tmpdir())), 'g'), replacement: '' }, - // The random suffix of mkdtemp-created assembly output dirs, e.g. "cdk.outAb12Cd" - { pattern: /cdk\.out[a-zA-Z0-9]{6}/g, replacement: 'cdk.out' }, + // The random suffix of mkdtemp-created assembly output dirs, e.g. + // "/cdk.outAb12Cd". Anchored on the path separator so prose that merely + // starts with "cdk.out" is never mangled; mkdtemp dirs always sit directly + // under the temp dir, so the separator is always present (and the TMP + // scrubber above has already run — scrubbers apply in array order). + { pattern: /\/cdk\.out[a-zA-Z0-9]{6}/g, replacement: '/cdk.out' }, ]; } diff --git a/packages/aws-cdk/test/commands/synth.test.ts b/packages/aws-cdk/test/commands/synth.test.ts index c7354207c..ebc639209 100644 --- a/packages/aws-cdk/test/commands/synth.test.ts +++ b/packages/aws-cdk/test/commands/synth.test.ts @@ -11,9 +11,15 @@ import { IoHostRecorder } from '../_helpers/io-recorder'; // `cdk synth` prints the template of a single selected stack to stdout (or a // success message for several stacks) and appends a feature-flags warning with // CI/quiet-dependent gating. Its stdout is a scripting surface (must stay valid -// YAML in CI mode), so these tests run the command end-to-end and snapshot +// YAML in CI mode), so these tests run `CdkToolkit.synth` and snapshot // everything the user sees (via IoHostRecorder) — the committed NDJSON is the // assertion for selection, template rendering, message levels and gating. +// +// Coverage notes: the cli.ts argument mapping (settings-file `quiet` override, +// the `--exclusively` branch) sits above the entry point used here and is NOT +// pinned by these snapshots. Several scenarios are also asserted, spy-style, in +// test/cli/cdk-toolkit.test.ts describe('synth') — these snapshots are the +// authoritative record of the user-visible output. describe('cdk synth', () => { const ioHost = CliIoHost.instance(); let recorder: IoHostRecorder; @@ -39,7 +45,6 @@ describe('cdk synth', () => { }); afterEach(() => { - ioHost.isCI = false; recorder.matchSnapshot(); }); @@ -127,7 +132,14 @@ describe('cdk synth', () => { // top-level stacks), so the snapshot pins the hierarchical-id handoff. await synth({ stacks: [STACK_A, STACK_B], - nestedAssemblies: [{ stacks: [{ ...STACK_WITH_ERROR, metadata: {}, stackName: 'nested', displayName: 'Test-Stack-A/nested' }] }], + nestedAssemblies: [{ + stacks: [{ + stackName: 'nested', + displayName: 'Test-Stack-A/nested', + template: { Resources: { TemplateName: 'nested' } }, + env: 'aws://123456789012/bermuda-triangle-1', + }], + }], }, ['Test-Stack-A-Display-Name', 'Test-Stack-A/nested']); }); @@ -165,21 +177,21 @@ describe('cdk synth', () => { test('explicitly selected stack with error annotations fails synthesis', async () => { await expect(synth({ stacks: [STACK_A, STACK_B], - nestedAssemblies: [{ stacks: [{ properties: { validateOnSynth: false }, ...STACK_WITH_ERROR }] }], + nestedAssemblies: [{ stacks: [STACK_WITH_ERROR] }], }, ['Test-Stack-A/witherrors'], { quiet: true })).rejects.toThrow(/Synthesis finished with errors/); }); test('a validateOnSynth stack with errors fails synth when validation is on', async () => { await expect(synth({ stacks: [STACK_A, STACK_B], - nestedAssemblies: [{ stacks: [{ properties: { validateOnSynth: true }, ...STACK_WITH_ERROR }] }], + nestedAssemblies: [{ stacks: [{ ...STACK_WITH_ERROR, properties: { validateOnSynth: true } }] }], }, [], { quiet: true, autoValidate: true })).rejects.toThrow(/Synthesis finished with errors/); }); test('a validateOnSynth stack with errors is tolerated with --no-validation', async () => { await synth({ stacks: [STACK_A, STACK_B], - nestedAssemblies: [{ stacks: [{ properties: { validateOnSynth: true }, ...STACK_WITH_ERROR }] }], + nestedAssemblies: [{ stacks: [{ ...STACK_WITH_ERROR, properties: { validateOnSynth: true } }] }], }, [], { quiet: true, autoValidate: false }); }); @@ -191,6 +203,10 @@ describe('cdk synth', () => { test('a stage-only app selects no stacks and still succeeds', async () => { // Pipeline-style apps have no top-level stacks; the no-pattern MainAssembly // default selects nothing and synth must not fail. + // + // The snapshot deliberately pins the current output as-is, including the + // awkward empty-parens "Supply a stack id ()" line — this is a baseline, + // not an endorsement; fixing that wording should show up as a snapshot diff. await synth({ stacks: [], nestedAssemblies: [{ stacks: [{ ...STACK_B, stackName: 'staged', displayName: 'MyStage/staged' }] }], @@ -227,7 +243,7 @@ describe('cdk synth', () => { test('--ignore-errors tolerates error annotations on a selected stack', async () => { await synth({ stacks: [STACK_A, STACK_B], - nestedAssemblies: [{ stacks: [{ properties: { validateOnSynth: false }, ...STACK_WITH_ERROR }] }], + nestedAssemblies: [{ stacks: [STACK_WITH_ERROR] }], }, ['Test-Stack-A/witherrors'], { quiet: true, ignoreErrors: true }); }); }); From dc1a033356a480733e290993f9eee1eb3240fa76 Mon Sep 17 00:00:00 2001 From: Ian Hou <45278651+iankhou@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:04:58 -0400 Subject: [PATCH 3/3] test(cli): match both path separators in the cdk.out scrubber The anchored scrubber only matched POSIX separators; on Windows the temp-dir scrubber leaves '\cdk.outAb12Cd', so the random suffix survived and the snapshots would be nondeterministic there (and differ from the committed '/' form). Match either separator and normalize to '/' in the replacement so the committed snapshots hold on both platforms. --- packages/aws-cdk/test/_helpers/io-recorder.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk/test/_helpers/io-recorder.ts b/packages/aws-cdk/test/_helpers/io-recorder.ts index de462f332..ea30177da 100644 --- a/packages/aws-cdk/test/_helpers/io-recorder.ts +++ b/packages/aws-cdk/test/_helpers/io-recorder.ts @@ -39,11 +39,12 @@ function defaultScrubbers(): Scrubber[] { // The OS temp dir (tests chdir into a temp dir) { pattern: new RegExp(escapeRegExp(fs.realpathSync(os.tmpdir())), 'g'), replacement: '' }, // The random suffix of mkdtemp-created assembly output dirs, e.g. - // "/cdk.outAb12Cd". Anchored on the path separator so prose that merely - // starts with "cdk.out" is never mangled; mkdtemp dirs always sit directly - // under the temp dir, so the separator is always present (and the TMP - // scrubber above has already run — scrubbers apply in array order). - { pattern: /\/cdk\.out[a-zA-Z0-9]{6}/g, replacement: '/cdk.out' }, + // "/cdk.outAb12Cd". Anchored on the path separator (either flavor, and + // normalized to '/' so snapshots match cross-platform) so prose that + // merely starts with "cdk.out" is never mangled; mkdtemp dirs always sit + // directly under the temp dir, so the separator is always present (and + // the TMP scrubber above has already run — scrubbers apply in array order). + { pattern: /[\\/]cdk\.out[a-zA-Z0-9]{6}/g, replacement: '/cdk.out' }, ]; }