Scaffold a new integration test for cdkd. Creates a minimal CDK app with the specified AWS resources for deploy/destroy E2E testing.
Install
npx skillscat add go-to-k/cdkd/new-integ Install via the SkillsCat registry.
New Integration Test Scaffold
You are scaffolding a new integration test for cdkd.
Input
The user provides a kebab-case test name (e.g., ses-email-identity,s3-event-notification).
Steps
Build cdkd first:
vp run buildfrom the repo root — verify.sh and the
synth check both run againstdist/cli.js, so source changes without a build
have no effect.Check the test does not already exist in
tests/integration/<test-name>/,
AND that an existing fixture does not already cover the pattern. Grep for the
construct/resource type acrosstests/integration/*/libbefore building —
a fixture that already deploys the thing (even without a verify.sh) means the
higher-value move is often to ADD a functional verify.sh to it rather than a
whole new fixture.Read a recent fixture as the reference shape. Use
tests/integration/dynamodb-gsi-update/(deploy + UPDATE + destroy with real
assertions) ortests/integration/s3-event-notification/(functional check)
— NOT the oldest ones, whose conventions have drifted.Create the test directory at
tests/integration/<test-name>/with these
files. The project runs CDK apps via Node 24 type-stripping (node bin/app.ts),
so the fixture is ESM ("type": "module") and imports carry the.ts
extension — there is NOts-node/tsxdependency.cdk.json:{ "app": "node bin/app.ts" }package.json(note"type": "module"; nots-node):{ "name": "cdkd-integ-<test-name>", "version": "1.0.0", "private": true, "description": "<one-line description>", "scripts": { "build": "tsc", "watch": "tsc -w" }, "devDependencies": { "@types/node": "^20.0.0", "aws-cdk": "^2.1112.0", "typescript": "^5.0.0" }, "dependencies": { "aws-cdk-lib": "^2.260.0", "constructs": "^10.0.0" }, "type": "module" }That
aws-cdk-libfloor is FENCED and is read from this file: it may not
sit below the lowest floor intests/integration/*/package.json, ortests/unit/scripts/integ-cdk-lib-floor.test.tsreds CI (issue
#2839). Raising it is always
safe; lowering it, or letting it stay put while the corpus moves past it,
reds CI. The rule, and why it is not an
equality fence, is in .claude/rules/testing.md.tsconfig.json— copy it verbatim from the reference fixture chosen in
step 3 (tests/integration/dynamodb-gsi-update/tsconfig.json). It is ESNext /
NodeNext withrewriteRelativeImportExtensions: true, which is what makes the.ts-suffixed relative imports type-check; do not hand-write a shorter one.bin/app.ts— the entry point, copied from the reference fixture. It
imports the stack WITH the.tsextension, names itCdkd<PascalCaseName>Example, and wiresenvfromCDK_DEFAULT_ACCOUNT/CDK_DEFAULT_REGION.lib/<test-name>-stack.ts— the stack. Keep it minimal: only the resource
under test + its required dependencies. In the class docstring, add acovers: AWS::Service::Typeline for each resource type the test is meant to
cover (the coverage matrix in step 7 parses these annotations).Ask the user what specific AWS resources the test should create, if not
obvious from the test name.Write a
verify.sh(the most important deliverable — a deploy-only smoke
test is NOT enough). Model it ontests/integration/dynamodb-gsi-update/verify.sh.
It must:set -euo pipefail,cd "$(dirname "$0")", and defineSTACK,REGION
(${AWS_REGION:-us-east-1}),STATE_KEY, andLOCAL_DIST="$(cd ../../../dist && pwd)/cli.js".Require
STATE_BUCKET(fail fast if unset) and the builtdist/cli.js.Define a
cleanup()and arm it on EXIT and both signals; run it once
up-front as a pre-run cleanup:trap cleanup EXIT trap '(exit 130); cleanup; exit 130' INT trap '(exit 143); cleanup; exit 143' TERMtrap cleanup EXIT INT TERMis NOT equivalent and must never be used — a
bash signal handler returns to the interrupted point, so the script resumes
the interrupted phase and canexit 0, reporting PASS while resources leak.
The(exit N)seed is also load-bearing: inside a handler$?is the
interrupted command's status, so acleanupopening withrc=$?would
otherwise see 0 and skip teardown. Disarm withtrap - EXIT INT TERM.
Enforced bytests/unit/scripts/integ-verify-signal-traps.test.ts(#1097).cleanup must
node "${LOCAL_DIST}" state destroy, delete
the AWS resources by deterministic name, remove thestate.json+lock.json,
and sweep/aws/lambda/${STACK}*log groups (Lambda auto-creates them on
invoke and neither CFn nor cdkd deletes them — leaving them counts as an
orphan). Any sweep that DELETES what a${VAR}-filtered listing returns
needs a scope guard that DOMINATES it — either the sweep sits inside the
non-catch-all arm, or it sits after anesacwhose catch-all leaves viaexit/return; acasethat merely warns and falls through stops
nothing. An empty variable makes the filter match everything,cleanup
runs underset +eu, and every delete is|| true, so the run would wipe
unrelated resources and still exit 0. Accepting arm first, a pattern that
cannot match empty, and a WARN containingteardown sweep refused.
Write YOUR OWN scope's shortest literal prefix plus?*—Cdkd?*is
what the already-guarded fixtures happen to need and is NOT a house
pattern. Plenty of fixtures use a stack name that does not start withCdkd(EventBridgeStack,CognitoStack, ...), and copyingCdkd?*into
one of those makes the guard refuse PERMANENTLY and silently, so the sweep
never runs, the orphans leak and the run still exits 0. See
docs/integ-fixture-conventions.md.
Convention only — nothing checks it yet.Phase 1 — deploy, then a functional assertion that the feature
actually works and reached AWS (curl the endpoint / put an object and
confirm the handler fired / read the property back via the AWS API). A clean
deploy summary is not proof.Phase 2 — UPDATE when the resource has a meaningful in-place change
(gate the stack onprocess.env.CDKD_TEST_UPDATE === 'true'). Set the env
PER-PHASE: Phase 1 baseline viaenv -u CDKD_TEST_UPDATE node ... deploy,
Phase 2 via inlineCDKD_TEST_UPDATE=true node ... deploy. Never gate the
whole phase on a caller-set global env. Assert the change reached AWS AND
that it was in-place, not a replacement (e.g. an identity field like
DynamoDBCreationDateTimeis unchanged).Final phase — destroy (
--force), then assert the resource is gone, thestate.jsonis gone, and sweep the log groups again. Gone-assertions MUST
go through the canonicalgone_probe/assert_gonehelper block (copy it
verbatim from any swept fixture, e.g.tests/integration/dynamodb-gsi-update/verify.sh; source of truth inscripts/check-integ-probe-not-found.ts), inserted right afterset -euo pipefail:assert_gone "resource <name> still exists after destroy" aws <service> <read-verb> [args...] assert_gone "state file ${STATE_KEY} still exists after destroy" aws s3api head-object --bucket "${STATE_BUCKET}" --key "${STATE_KEY}"Never write
if aws <read-probe> ... >/dev/null 2>&1; then FAIL(or the
inverseif ! aws ...; then <gone>): a throttled probe would silently
pass the leak check (issue #1097 pattern 2). The same goes for the
capture-form spellingN=$(aws <read-verb> ... 2>/dev/null || echo 0)
(|| trueincluded) and for silenced probe wrappers
(fn() { aws ... >/dev/null 2>&1; }) — use plain strict captures, or agone_probebranch when not-found is a legitimate outcome (issue #1120).
In a multi-statement value wrapper, append|| return 1to every
INTERMEDIATEout="$(aws ...)"capture (errexit is cleared inside$( ), so only the last command's status propagates otherwise), and
write best-effort cleanup helpers asfn() { ( set +eu; ... ) }so they
never re-arm strict mode in aset +eucaller.
Uses3api head-objectfor state files, neveraws s3 ls(exit 1 +
empty output for "no keys" is indistinguishable from a silenced error).
Enforced bytests/unit/scripts/integ-verify-probe-not-found.test.ts.End with a single
echo "[verify] PASS — ..."line (the run-integ harness
greps for it).chmod +x verify.sh.
Regenerate the coverage matrices (adding/removing a Construct changes
them, and CI hard-fails on a stale matrix):vp run gen:all-matrices && vp run formatCommit the regenerated
docs/_generated/*.jsonalongside the fixture.Install deps + verify synthesis:
cd tests/integration/<test-name> && npm install node ../../../dist/cli.js synth --region us-east-1Run the full test with
/run-integ <test-name>(deploy + functional +
destroy + orphan-zero verification against real AWS). A fixture that has never
passed/run-integis worse than no fixture — never commit one unrun.
Important
- Keep tests minimal — only the resources needed to verify the behavior.
- English only — this is an OSS repo; no Japanese characters in any committed
file (code, comments, shell scripts, READMEs). - Do NOT include
stateBucketincdk.jsoncontext (let the CLI resolve it). - Use
RemovalPolicy.DESTROY(andautoDeleteObjects: trueon buckets) so
teardown is clean. - Prefer
lambda.Code.fromInline(...)for handlers — no asset bundling needed,
and@aws-sdk/client-*is already in the Node.js 20 runtime. - Stack id convention:
Cdkd<PascalCaseName>Example(matches theSTACKvar in
verify.sh). Some older fixtures use<PascalCaseName>Stack; follow theCdkd...Exampleform for new tests. - Do NOT commit
node_modules/,package-lock.json,cdk.out/, or build output
(*.js/*.d.ts) —tests/integration/.gitignorecovers them.