go-to-k

new-integ

Scaffold a new integration test for cdkd. Creates a minimal CDK app with the specified AWS resources for deploy/destroy E2E testing.

go-to-k 147 12 Updated 4w ago
GitHub

Install

npx skillscat add go-to-k/cdkd/new-integ

Install via the SkillsCat registry.

SKILL.md

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/<test-name>/,
    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/<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 NO ts-node / tsx dependency.

    cdk.json:

    {
      "app": "node bin/app.ts"
    }

    package.json (note "type": "module"; no ts-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-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). 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 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
    Cdkd<PascalCaseName>Example, and wires env from CDK_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 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:

      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.
      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:

      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
      inverse if ! aws ...; then <gone>): a throttled probe would silently
      pass the leak check (issue #1097 pattern 2). The same goes for the
      capture-form spelling N=$(aws <read-verb> ... 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):

    vp run gen:all-matrices && vp run format

    Commit the regenerated docs/_generated/*.json alongside the fixture.

  8. Install deps + verify synthesis:

    cd tests/integration/<test-name> && npm install
    node ../../../dist/cli.js synth --region us-east-1
  9. Run the full test with /run-integ <test-name> (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: Cdkd<PascalCaseName>Example (matches the STACK var in
    verify.sh). Some older fixtures use <PascalCaseName>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.