--- name: new-integ description: Scaffold a new integration test for cdkd. Creates a minimal CDK app with the specified AWS resources for deploy/destroy E2E testing. argument-hint: "" --- # 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 1. **Build cdkd first**: `vp run build` from the repo root — verify.sh and the synth check both run against `dist/cli.js`, so source changes without a build have no effect. 2. **Check the test does not already exist** in `tests/integration//`, AND that an existing fixture does not already cover the pattern. Grep for the construct/resource type across `tests/integration/*/lib` before 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. 3. **Read a recent fixture as the reference shape.** Use `tests/integration/dynamodb-gsi-update/` (deploy + UPDATE + destroy with real assertions) or `tests/integration/s3-event-notification/` (functional check) — NOT the oldest ones, whose conventions have drifted. 4. **Create the test directory** at `tests/integration//` 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 NO `ts-node` / `tsx` dependency. **`cdk.json`**: ```json { "app": "node bin/app.ts" } ``` **`package.json`** (note `"type": "module"`; no `ts-node`): ```json { "name": "cdkd-integ-", "version": "1.0.0", "private": true, "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-lib` floor is FENCED and is read from this file**: it may not sit below the lowest floor in `tests/integration/*/package.json`, or `tests/unit/scripts/integ-cdk-lib-floor.test.ts` reds CI (issue [#2839](https://github.com/go-to-k/cdkd/issues/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](../../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 with `rewriteRelativeImportExtensions: 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 `.ts` extension, names it `CdkdExample`, and wires `env` from `CDK_DEFAULT_ACCOUNT` / `CDK_DEFAULT_REGION`. **`lib/-stack.ts`** — the stack. Keep it minimal: only the resource under test + its required dependencies. In the class docstring, add a `covers: AWS::Service::Type` line for each resource type the test is meant to cover (the coverage matrix in step 7 parses these annotations). 5. **Ask the user** what specific AWS resources the test should create, if not obvious from the test name. 6. **Write a `verify.sh`** (the most important deliverable — a deploy-only smoke test is NOT enough). Model it on `tests/integration/dynamodb-gsi-update/verify.sh`. It must: - `set -euo pipefail`, `cd "$(dirname "$0")"`, and define `STACK`, `REGION` (`${AWS_REGION:-us-east-1}`), `STATE_KEY`, and `LOCAL_DIST="$(cd ../../../dist && pwd)/cli.js"`. - Require `STATE_BUCKET` (fail fast if unset) and the built `dist/cli.js`. - Define a `cleanup()` and arm it on EXIT **and both signals**; run it once up-front as a pre-run cleanup: ```bash trap cleanup EXIT trap '(exit 130); cleanup; exit 130' INT trap '(exit 143); cleanup; exit 143' TERM ``` `trap cleanup EXIT INT TERM` is NOT equivalent and must never be used — a bash signal handler returns to the interrupted point, so the script resumes the interrupted phase and can `exit 0`, reporting PASS while resources leak. The `(exit N)` seed is also load-bearing: inside a handler `$?` is the interrupted command's status, so a `cleanup` opening with `rc=$?` would otherwise see 0 and skip teardown. Disarm with `trap - EXIT INT TERM`. Enforced by `tests/unit/scripts/integ-verify-signal-traps.test.ts` (#1097). cleanup must `node "${LOCAL_DIST}" state destroy`, delete the AWS resources by deterministic name, remove the `state.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 an `esac` whose catch-all leaves via `exit` / `return`; a `case` that merely warns and falls through stops nothing. An empty variable makes the filter match everything, `cleanup` runs under `set +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 containing `teardown 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 with `Cdkd` (`EventBridgeStack`, `CognitoStack`, ...), and copying `Cdkd?*` 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](../../../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 on `process.env.CDKD_TEST_UPDATE === 'true'`). Set the env PER-PHASE: Phase 1 baseline via `env -u CDKD_TEST_UPDATE node ... deploy`, Phase 2 via inline `CDKD_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 DynamoDB `CreationDateTime` is unchanged). - **Final phase — destroy** (`--force`), then assert the resource is gone, the `state.json` is gone, and sweep the log groups again. Gone-assertions MUST go through the canonical `gone_probe` / `assert_gone` helper block (copy it verbatim from any swept fixture, e.g. `tests/integration/dynamodb-gsi-update/verify.sh`; source of truth in `scripts/check-integ-probe-not-found.ts`), inserted right after `set -euo pipefail`: ```bash assert_gone "resource still exists after destroy" aws [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 ... >/dev/null 2>&1; then FAIL` (or the inverse `if ! aws ...; then `): a throttled probe would silently pass the leak check (issue #1097 pattern 2). The same goes for the capture-form spelling `N=$(aws ... 2>/dev/null || echo 0)` (`|| true` included) and for silenced probe wrappers (`fn() { aws ... >/dev/null 2>&1; }`) — use plain strict captures, or a `gone_probe` branch when not-found is a legitimate outcome (issue #1120). In a multi-statement value wrapper, append `|| return 1` to every INTERMEDIATE `out="$(aws ...)"` capture (errexit is cleared inside `$( )`, so only the last command's status propagates otherwise), and write best-effort cleanup helpers as `fn() { ( set +eu; ... ) }` so they never re-arm strict mode in a `set +eu` caller. Use `s3api head-object` for state files, never `aws s3 ls` (exit 1 + empty output for "no keys" is indistinguishable from a silenced error). Enforced by `tests/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`. 7. **Regenerate the coverage matrices** (adding/removing a Construct changes them, and CI hard-fails on a stale matrix): ```bash vp run gen:all-matrices && vp run format ``` Commit the regenerated `docs/_generated/*.json` alongside the fixture. 8. **Install deps + verify synthesis**: ```bash cd tests/integration/ && npm install node ../../../dist/cli.js synth --region us-east-1 ``` 9. **Run the full test** with `/run-integ ` (deploy + functional + destroy + orphan-zero verification against real AWS). A fixture that has never passed `/run-integ` is 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 `stateBucket` in `cdk.json` context (let the CLI resolve it). - Use `RemovalPolicy.DESTROY` (and `autoDeleteObjects: true` on 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: `CdkdExample` (matches the `STACK` var in verify.sh). Some older fixtures use `Stack`; follow the `Cdkd...Example` form for new tests. - Do NOT commit `node_modules/`, `package-lock.json`, `cdk.out/`, or build output (`*.js` / `*.d.ts`) — `tests/integration/.gitignore` covers them.