diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..1064a333 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,106 @@ +name: e2e + +on: + push: + branches: [main] + paths: + - 'src/**' + - 'e2e/**' + - 'package.json' + - 'yarn.lock' + - '.github/workflows/e2e.yml' + pull_request: + paths: + - 'src/**' + - 'e2e/**' + - 'package.json' + - 'yarn.lock' + - '.github/workflows/e2e.yml' + workflow_dispatch: + +# One in-flight e2e run per ref; cancel older runs so ephemeral resources don't pile up. +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + id-token: write + +jobs: + check-lambda-changes: + name: Check lambda changes + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.filter.outputs.lambda }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 + id: filter + with: + filters: | + lambda: + - 'src/**' + - 'e2e/**' + - '.github/workflows/e2e.yml' + + e2e-lambda: + name: Lambda e2e (Node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + needs: [check-lambda-changes] + strategy: + matrix: + node-version: [20] + permissions: + contents: read + id-token: write + env: + FORCE_COLOR: 1 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Set up Node ${{ matrix.node-version }} + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Set up Yarn + run: corepack enable && corepack prepare yarn@4.10.3 --activate + + - name: Install plugin dependencies + run: yarn install --immutable + + # OIDC federation: GitHub -> AWS. The role is assumable only from this repo's + # workflows and scoped to the e2e sandbox account. + - name: AWS auth (OIDC) + if: needs.check-lambda-changes.outputs.should_run == 'true' + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + with: + role-to-assume: ${{ vars.AWS_ROLE_ARN_E2E }} + aws-region: ${{ vars.AWS_REGION_E2E || 'ap-northeast-3' }} + + - name: Install e2e dependencies + run: npm install + working-directory: e2e + + # Short-lived Datadog API + App keys via OIDC federation (dd-sts), governed by + # the serverless-plugin-datadog-e2e policy. No static Datadog keys in this repo. + - name: Get Datadog credentials (dd-sts) + id: dd-sts + if: needs.check-lambda-changes.outputs.should_run == 'true' + uses: DataDog/dd-sts-action@2e8187910199bd93129520183c093e19aa585c75 # v1.0.0 + with: + policy: serverless-plugin-datadog-e2e + + - name: Run e2e suite + run: npm test + working-directory: e2e + env: + DATADOG_API_KEY: ${{ steps.dd-sts.outputs.api_key }} + DATADOG_APP_KEY: ${{ steps.dd-sts.outputs.app_key }} + DATADOG_SITE: ${{ vars.DD_SITE_E2E || 'datadoghq.com' }} + AWS_REGION: ${{ vars.AWS_REGION_E2E || 'ap-northeast-3' }} + # Skipped only when nothing relevant changed. When relevant files change the + # suite runs for real and the dd-sts / AWS OIDC steps must succeed -- an + # auth/federation failure fails the job loudly rather than skipping green. + SKIP_LAMBDA_TESTS: ${{ needs.check-lambda-changes.outputs.should_run != 'true' }} diff --git a/e2e/.env.local.example b/e2e/.env.local.example new file mode 100644 index 00000000..da531b05 --- /dev/null +++ b/e2e/.env.local.example @@ -0,0 +1,17 @@ +# Copy to e2e/.env.local (gitignored) for local runs. Real shell env vars take +# precedence, so this file is purely a convenience. + +# Datadog API + APP keys. The API key is wired into the Lambda extension AND used +# to authenticate the API client; the APP key is needed to poll spans/logs. +DATADOG_API_KEY= +DATADOG_APP_KEY= + +# Datadog site the org lives in (datadoghq.com, datadoghq.eu, us3.datadoghq.com, ...). +DATADOG_SITE=datadoghq.com + +# AWS region to deploy the ephemeral Lambda into. Must have pinned layers in +# ../src/layers.json (ap-northeast-3 is the default). +AWS_REGION=ap-northeast-3 + +# Set to skip the suite entirely. +# SKIP_LAMBDA_TESTS=true diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 00000000..3c16a963 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,7 @@ +node_modules +.env.local +fixtures/**/node_modules +fixtures/**/.serverless +fixtures/**/package-lock.json +serverless-plugin-datadog-*.tgz +*.log diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..66c3833f --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,101 @@ +# serverless-plugin-datadog e2e suite + +End-to-end coverage for the AWS Lambda instrumentation this plugin performs. It +deploys a real, ephemeral Lambda with the plugin enabled, verifies the deployed +config and the telemetry it produces in Datadog, proves re-deploy is idempotent, +then tears the stack down and verifies a clean end-state. + +Conforms to the shared contract in `serverless-ci/e2e/spec.md`; mirrors the +`datadog-ci` reference suite (`e2e/cloud-run.test.ts` + `e2e/helpers/*`). + +## What it does + +``` +sls deploy (APPLY: provision + instrument) -> verify CONFIG + -> aws lambda invoke (trigger) -> verify TELEMETRY (traces + logs) + -> sls deploy again -> assert IDEMPOTENT (no diff/dup) + -> sls remove (REMOVE) -> verify CLEAN (function gone) + -> teardown (always, even on failure) +``` + +For this tool the plugin runs as part of `sls deploy`, so provisioning the +uninstrumented workload and APPLY are the same step. REMOVE deletes the whole +CloudFormation stack, so the clean end-state is the function (and all its DD +config) being absent -- asserted explicitly. + +**Config verified** (`helpers/lambda-verifier.ts`): the pinned Datadog Node layer ++ extension layer (versions read from `../src/layers.json`, so drift blames the +plugin), the redirected handler with the original preserved in `DD_LAMBDA_HANDLER`, +the required `DD_*` env vars, and the `service` / `env` / `version` / `dd_sls_plugin` +tags. Identity (run-id service name, env, version) is asserted -- not mere presence. + +**Telemetry verified** (`helpers/lambda-telemetry-checker.ts`): spans and logs are +polled (15s × 20) filtered by the unique service name, and the matched records must +carry the full identity (service + env + version), not just exist. + +## Resource hygiene + +Every run uses a unique name `one-e2e-slsplugin-lambda-` and stamps a +`one_e2e_created:` tag at creation (`helpers/naming.ts`). The shared +cross-repo sweeper ages out anything older than the grace window. In-test teardown +runs in `afterAll` regardless of outcome. + +## Prerequisites + +- **Node 20** and **npm** (the suite is a standalone npm project, isolated from the + plugin's Yarn Berry setup). +- The plugin is built and the fixture is installed automatically by `pretest` + (`npm test` runs `yarn build` at the repo root, then `npm install` in the fixture). +- **AWS auth** with permission to deploy Lambda / CloudFormation in the target + account. Locally, wrap the run with `aws-vault`: + ``` + aws-vault exec sso-serverless-sandbox-account-admin -- npm test + ``` + In CI, credentials come from GitHub→AWS OIDC (no static keys). +- **Datadog keys**: `DATADOG_API_KEY` (wired into the extension and used for the API + client) and `DATADOG_APP_KEY` (used to poll spans/logs). + +## Run locally + +``` +cd e2e +npm install + +# aws-vault provides AWS creds; dd-auth mints short-lived keys for the org -- no pasted keys. +aws-vault exec sso-serverless-sandbox-account-admin -- \ + dd-auth --domain app.datadoghq.com -- bash -c ' + export DATADOG_API_KEY="$DD_API_KEY" DATADOG_APP_KEY="$DD_APP_KEY" + npm test + ' +``` + +`dd-auth` injects `$DD_API_KEY` / `$DD_APP_KEY` into the wrapped subprocess; the +suite reads them as `DATADOG_API_KEY` / `DATADOG_APP_KEY`. AWS credentials still come +from `aws-vault`. Set `SKIP_LAMBDA_TESTS=true` to skip the suite. + +## Configuration + +| Env var | Required | Default | Purpose | +| ------------------ | -------- | ---------------- | ---------------------------------------------- | +| `DATADOG_API_KEY` | yes | -- | Wired into the extension + API-client auth | +| `DATADOG_APP_KEY` | yes | -- | API-client auth for span/log polling | +| `DATADOG_SITE` | no | `datadoghq.com` | Datadog site | +| `AWS_REGION` | no | `ap-northeast-3` | Deploy region (must be pinned in `layers.json`)| +| `SKIP_LAMBDA_TESTS`| no | -- | `true` skips the suite | + +(AWS credentials come from the ambient AWS env / `aws-vault` / OIDC.) + +## CI + +`.github/workflows/e2e.yml` runs the suite behind a `dorny/paths-filter` gate +(`src/**`, `e2e/**`, the workflow file) and the `SKIP_LAMBDA_TESTS` flag, with +GitHub→AWS OIDC (`aws-actions/configure-aws-credentials`). Required repo settings: + +- Datadog auth (dd-sts): short-lived API + App keys minted at runtime via + [`DataDog/dd-sts-action`](https://github.com/DataDog/dd-sts-action) under the + `serverless-plugin-datadog-e2e` policy -- no static Datadog keys in this repo +- Variables: `AWS_ROLE_ARN_E2E` (the OIDC deploy role), `AWS_REGION_E2E` (default + `ap-northeast-3`), optionally `DD_SITE_E2E` + +The OIDC deploy role and the policy backing it are cataloged in +`serverless-ci/e2e/iam-infra.md`. diff --git a/e2e/helpers/e2e.config.ts b/e2e/helpers/e2e.config.ts new file mode 100644 index 00000000..0f28095c --- /dev/null +++ b/e2e/helpers/e2e.config.ts @@ -0,0 +1,86 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed + * under the Apache License Version 2.0. + * + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2026 Datadog, Inc. + */ + +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import {fileURLToPath} from 'node:url'; + +import {type E2ENaming} from './naming'; +import {type ExpectedLayers, type LambdaVerifierConfig} from './lambda-verifier'; + +// Repo-local config feeding the shared e2e helpers. This file is NOT synced -- it holds +// everything specific to serverless-plugin-datadog that the shared, parameterized helpers +// read through their config arguments. + +export const NAMING: E2ENaming = {tool: 'slsplugin', platform: 'lambda'}; + +export const ENV_NAME = 'e2e'; +export const ENV_VERSION = '1.0.0'; +const RUNTIME = 'nodejs20.x'; + +// Transient cloud-provider errors safe to retry, passed as ExecOptions.retryPatterns. +export const RETRY_PATTERNS = [ + // Generic / cross-cloud + 'GatewayTimeout', + 'Operation was canceled', + 'ETIMEDOUT', + 'ECONNRESET', + 'temporarily unavailable', + // AWS Lambda / CloudFormation / STS + 'ThrottlingException', + 'TooManyRequestsException', + 'Rate exceeded', + 'RequestLimitExceeded', + 'ResourceConflictException', + 'ServiceException', + 'InternalFailure', + 'ServiceUnavailable', + 'is in progress', // CloudFormation stack op already running + 'ProvisionedConcurrencyConfig', // eventual-consistency churn on update +]; + +// `sls deploy` names functions `--`; stage is pinned to `e2e` and the +// only function is `hello`. +export const functionName = (serviceName: string): string => `${serviceName}-e2e-hello`; + +// Pinned artifact versions come from the plugin's own src/layers.json, so a version +// mismatch blames the plugin/registry, not upstream drift. +const expectedLayerArns = (region: string): ExpectedLayers => { + const layersPath = fileURLToPath(new URL('../../src/layers.json', import.meta.url)); + const layers = JSON.parse(fs.readFileSync(layersPath, 'utf-8')) as { + regions: Record>; + }; + const regionLayers = layers.regions[region]; + assert.ok(regionLayers, `region ${region} not present in src/layers.json`); + const node = regionLayers[RUNTIME]; + const extension = regionLayers.extension; + assert.ok(node, `no ${RUNTIME} layer pinned for ${region} in src/layers.json`); + assert.ok(extension, `no extension layer pinned for ${region} in src/layers.json`); + + return {node, extension}; +}; + +export const VERIFIER: LambdaVerifierConfig = { + functionName, + expectedLayerArns, + redirectHandler: '/opt/nodejs/node_modules/datadog-lambda-js/handler.handler', + originalHandler: 'handler.handler', + // With the extension enabled the plugin tags the function with its own version marker. + toolTag: {key: 'dd_sls_plugin', pattern: /^v\d+\.\d+\.\d+/}, + env: { + apiKeyVars: ['DD_API_KEY', 'DD_API_KEY_SECRET_ARN', 'DD_KMS_API_KEY', 'DD_API_KEY_SSM_ARN'], + present: ['DD_SITE'], + values: (serviceName) => ({ + DD_TRACE_ENABLED: 'true', + DD_SERVERLESS_LOGS_ENABLED: 'true', + DD_SERVICE: serviceName, + DD_ENV: ENV_NAME, + DD_VERSION: ENV_VERSION, + }), + }, +}; diff --git a/e2e/lambda.test.ts b/e2e/lambda.test.ts new file mode 100644 index 00000000..316e7d84 --- /dev/null +++ b/e2e/lambda.test.ts @@ -0,0 +1,144 @@ +import assert from 'node:assert/strict'; +import os from 'node:os'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +import {afterAll, beforeAll, describe, it} from 'vitest'; + +import {ENV_NAME, ENV_VERSION, functionName, NAMING, RETRY_PATTERNS, VERIFIER} from './helpers/e2e.config'; +import {execPromise, execPromiseWithRetries} from './helpers/exec'; +import {checkTelemetryFlowing} from './helpers/lambda-telemetry-checker'; +import {functionSnapshot, verifyInstrumented, verifyUninstrumented, type FunctionSnapshot} from './helpers/lambda-verifier'; +import {freshnessTimestamp, namePrefix, newRunId} from './helpers/naming'; + +// Full lifecycle for the serverless-plugin-datadog AWS Lambda instrumentation: +// +// sls deploy (APPLY: provision + instrument) -> verify CONFIG +// -> invoke (trigger) -> verify TELEMETRY flows +// -> sls deploy again -> assert IDEMPOTENT (no diff) +// -> sls remove (REMOVE) -> verify CLEAN end-state +// -> teardown (afterAll, always) +// +// For this tool the plugin runs as part of `sls deploy`, so provisioning the +// uninstrumented workload and APPLY coincide -- there is no separately-deployed +// uninstrumented state to instrument later. REMOVE tears down the whole stack, +// so the clean end-state is the function (and all its DD config) being gone. + +const e2eDir = path.dirname(fileURLToPath(import.meta.url)); +const fixtureDir = path.join(e2eDir, 'fixtures', 'lambda-node'); + +const DEPLOY_TIMEOUT_MS = 900_000; +const TELEMETRY_TIMEOUT_MS = 600_000; + +const describeOrSkip = process.env.SKIP_LAMBDA_TESTS === 'true' ? describe.skip : describe; + +describeOrSkip('serverless-plugin-datadog lambda e2e', () => { + const region = process.env.AWS_REGION ?? 'ap-northeast-3'; + const runId = newRunId(); + const serviceName = namePrefix(NAMING, runId); + const apiKey = process.env.DATADOG_API_KEY ?? process.env.DD_API_KEY; + const appKey = process.env.DATADOG_APP_KEY ?? process.env.DD_APP_KEY; + const site = process.env.DATADOG_SITE ?? process.env.DD_SITE ?? 'datadoghq.com'; + + // Injected into `sls deploy` so serverless.yml stays static. Carries the unique + // name + freshness stamp (set atomically at creation) and the DD wiring inputs. + const deployEnv: Record = { + E2E_SERVICE_NAME: serviceName, + E2E_CREATED_TS: freshnessTimestamp(), + AWS_REGION: region, + DD_API_KEY: apiKey, + DD_SITE: site, + }; + const slsOptions = {env: deployEnv, cwd: fixtureDir}; + + const deploy = () => + execPromiseWithRetries('npx --no-install serverless deploy --stage e2e --conceal', { + ...slsOptions, + retryPatterns: RETRY_PATTERNS, + maxAttempts: 2, + delaySeconds: 20, + }); + + let firstSnapshot: FunctionSnapshot; + + beforeAll(() => { + assert.ok(apiKey, 'DATADOG_API_KEY (or DD_API_KEY) must be set: used to wire the extension and authenticate the API client'); + assert.ok(appKey, 'DATADOG_APP_KEY (or DD_APP_KEY) must be set: used to poll spans/logs from the Datadog API'); + // eslint-disable-next-line no-console + console.log(`Run id ${runId} -> service "${serviceName}" in ${region} (site ${site})`); + }); + + afterAll(async () => { + // Teardown always runs, even if a test above failed mid-lifecycle. + const result = await execPromise('npx --no-install serverless remove --stage e2e', slsOptions); + if (result.exitCode !== 0) { + // eslint-disable-next-line no-console + console.warn(`Teardown remove returned ${result.exitCode} (ok if already removed): ${result.stderr}`); + } + }); + + it( + 'deploys and instruments the function', + async () => { + const result = await deploy(); + assert.equal(result.exitCode, 0, `sls deploy failed: ${result.stderr || result.stdout}`); + + await verifyInstrumented(VERIFIER, serviceName, region); + firstSnapshot = await functionSnapshot(VERIFIER, serviceName, region); + }, + DEPLOY_TIMEOUT_MS, + ); + + it( + 'flows traces and logs after invocation', + async () => { + const outFile = path.join(os.tmpdir(), `${serviceName}-invoke.json`); + // A few invocations to give the extension something to flush promptly. + for (let i = 0; i < 3; i++) { + const result = await execPromiseWithRetries( + `aws lambda invoke --function-name "${functionName(serviceName)}" --region "${region}"` + + ` --payload '{}' --cli-binary-format raw-in-base64-out --output json "${outFile}"`, + {retryPatterns: RETRY_PATTERNS}, + ); + assert.equal(result.exitCode, 0, `lambda invoke failed: ${result.stderr}`); + const meta = JSON.parse(result.stdout) as {StatusCode?: number; FunctionError?: string}; + assert.equal(meta.StatusCode, 200, `unexpected invoke status: ${result.stdout}`); + assert.ok(!meta.FunctionError, `invocation errored: ${meta.FunctionError}`); + } + + await checkTelemetryFlowing({serviceName, env: ENV_NAME, version: ENV_VERSION, runId}); + }, + TELEMETRY_TIMEOUT_MS, + ); + + it( + 're-applies idempotently (no diff, no duplicate)', + async () => { + const result = await deploy(); + assert.equal(result.exitCode, 0, `re-deploy failed: ${result.stderr || result.stdout}`); + + // Still instrumented, still no double-wrap / duplicate layers... + await verifyInstrumented(VERIFIER, serviceName, region); + // ...and byte-for-byte the same instrumentation as the first apply. + const secondSnapshot = await functionSnapshot(VERIFIER, serviceName, region); + assert.deepEqual(secondSnapshot, firstSnapshot, 're-apply changed the function config'); + }, + DEPLOY_TIMEOUT_MS, + ); + + it( + 'removes cleanly with no residue', + async () => { + const result = await execPromiseWithRetries('npx --no-install serverless remove --stage e2e', { + ...slsOptions, + retryPatterns: RETRY_PATTERNS, + maxAttempts: 2, + delaySeconds: 20, + }); + assert.equal(result.exitCode, 0, `sls remove failed: ${result.stderr || result.stdout}`); + + await verifyUninstrumented(VERIFIER, serviceName, region); + }, + DEPLOY_TIMEOUT_MS, + ); +}); diff --git a/e2e/load-env.ts b/e2e/load-env.ts new file mode 100644 index 00000000..14185af5 --- /dev/null +++ b/e2e/load-env.ts @@ -0,0 +1,28 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +// Local convenience: load e2e/.env.local (gitignored) into process.env without a +// dependency. Real environment variables always win, so this is a no-op in CI. +const envPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '.env.local'); + +if (fs.existsSync(envPath)) { + for (const line of fs.readFileSync(envPath, 'utf-8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + const idx = trimmed.indexOf('='); + if (idx === -1) { + continue; + } + const key = trimmed.slice(0, idx).trim(); + let value = trimmed.slice(idx + 1).trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + if (!(key in process.env)) { + process.env[key] = value; + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 00000000..68a89623 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,19 @@ +{ + "name": "serverless-plugin-datadog-e2e", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "End-to-end suite for the serverless-plugin-datadog AWS Lambda instrumentation.", + "scripts": { + "setup": "bash setup.sh", + "pretest": "npm run setup", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@datadog/datadog-api-client": "^1.40.0", + "@types/node": "^20.19.0", + "typescript": "^5.9.3", + "vitest": "^2.1.9" + } +} diff --git a/e2e/setup.sh b/e2e/setup.sh new file mode 100644 index 00000000..bd8191ca --- /dev/null +++ b/e2e/setup.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Builds the plugin, packs it into a tarball, and installs it into the workload +# fixture alongside the Serverless Framework. +# +# Why a tarball and not `file:../../..`: npm's file: protocol whole-dir-links the +# target, and the repo root contains this fixture -- which would link back to the +# repo, recursing forever. A packed tarball respects .npmignore (dist only) and +# extracts cleanly, the same approach the datadog-ci e2e suite uses for artifacts. +set -euo pipefail + +cd "$(dirname "$0")" +E2E_DIR="$PWD" + +echo "==> Building plugin" +(cd .. && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 yarn build) + +echo "==> Packing plugin" +rm -f "$E2E_DIR"/serverless-plugin-datadog-*.tgz +TARBALL_NAME=$(cd .. && npm pack --silent --pack-destination "$E2E_DIR") +TARBALL="$E2E_DIR/$TARBALL_NAME" +echo " packed $TARBALL_NAME" + +echo "==> Installing workload fixture" +cd fixtures/lambda-node +npm install --no-audit --no-fund +# --no-save so the committed fixture package.json stays free of a local tarball path. +npm install --no-audit --no-fund --no-save "$TARBALL" + +echo "==> Setup complete" diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 00000000..f7650ff9 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "resolveJsonModule": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "fixtures"] +} diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts new file mode 100644 index 00000000..c6e7a56e --- /dev/null +++ b/e2e/vitest.config.ts @@ -0,0 +1,15 @@ +import {defineConfig} from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['*.test.ts'], + // Cloud round-trips are slow; defaults are overridden per test/hook too. + testTimeout: 600_000, + hookTimeout: 900_000, + // The lifecycle tests share deployed state and must run in declared order. + fileParallelism: false, + pool: 'forks', + // Loads e2e/.env.local for local runs (no-op in CI, real env wins). + setupFiles: ['./load-env.ts'], + }, +});