git:20261014.105149e to git:20261020.8d47d7a

94 added, 174 removed. Audit A to A.

- # testing Agent Notes
-
- Scoped instructions for `packages/testing/`. Read together with the
- [root `AGENTS.md`](../../AGENTS.md).
-
- ## What's in here
-
- `@zitadel/testing` is the customer-facing test kit — "Testcontainers for
- Zitadel": it boots a real local instance (`startLocalZitadel` /
- `connectZitadel`), orchestrates instance + app for Playwright
- (`withZitadel`), seeds users and sessions (`seed.*`, `authenticatedPage`),
- completes passkey ceremonies headlessly (`enableVirtualPasskey`), and
- drives the `<zitadel-login>` widget through whole auth ceremonies
- (`loginWithPassword`, `registerWithPasskey`, …, with `flowAction` /
- `flowField` as escape hatches).
-
- In-repo consumers, which double as the dogfood proof:
+ # Agent Instructions — `packages/testing`
- - `e2e/demo-next` (`e2e:demo-next-real`) — flagship Playwright consumer.
- - `e2e/console` (`e2e:console-real`) and `console:dev-real` — kit as suite
- bootstrapper and as dev-loop seeder.
- - `e2e/journey` — the product journey imports kit helpers as a
- workspace dep; its testkit variant installs the kit **from the local
- registry** exactly as a customer would. Breaking the customer install
- path shows up there, not in unit tests.
+ `@zitadel/testing` is the customer-facing test kit: it boots or connects to a
+ real instance and drives it, and the login, from Playwright. Its API:
+ [`README.md`](README.md). The repo dogfoods it in `e2e/demo-next`'s real lane,
+ `e2e/console`, `console:dev-real` and `e2e/journey`, whose testkit variant
+ installs the kit from the local registry as a customer would.
- ## It is published — act like it
+ ## Which layer a test belongs in
- On the release train since #692: listed in `PUBLIC_RELEASE_PACKAGES`
- ([`scripts/release-manifest.mjs`](../../scripts/release-manifest.mjs)) and in
- the changesets `fixed` group — the two lists must match **in order**
- (enforced by `apps/cli/tests/unit/scripts/release-manifest.test.ts`).
- Consequences:
+ Test at the lowest layer that proves the property, and never duplicate it
+ upward.
- - **Every observable change needs a changeset** (minor for new API surface),
- and PR titles follow the outcome-based decision table in
- [`.changeset/README.md`](../../.changeset/README.md). The kit-specific
- trap: don't reach for `test:` because the package is *about* testing —
- it is shipped product surface, so a new capability is `feat` and a
- repair is `fix`.
- - **The `alpha` dist-tag is intentional.** The train publishes in changesets
- pre mode, so releases land on `npm i @zitadel/testing@alpha`; `latest`
- deliberately sits at the `0.0.0` bootstrap placeholder until pre mode
- ends. Do not "fix" the tag, and do not flag it in review.
- - `build-release` (in `moon.yml`) precleans via `release-clean.mjs` and
- rebuilds; it is registered in `tools/release/moon.yml`'s
- `build-public-packages` aggregate and `release:check-graph` enforces
- that registration. It needs no mutex: nothing in the release graph reads
- this package's dist, task deps alone order it.
+ 1. **Unit** (Vitest, jsdom): markup, props, ARIA, slot projection, event
+ shapes, pure logic.
+ 2. **Browser** (Vitest in Chromium, `*.browser.spec.ts`): form association,
+ focus delegation, Enter-to-submit, anything needing a real
+ `ElementInternals` or `HTMLFormElement`.
+ 3. **End to end** (Playwright, the root `e2e/` project): only what is
+ exclusively framework integration (middleware, the `/__nextgen` proxy, a
+ real `Set-Cookie` round trip, full-page navigation). Component, atom and
+ orchestrator behaviour stays in Vitest. The consumer journey
+ (`e2e/journey/`) runs a freshly generated app; the docs site's URL-based
+ suite lives beside its app, in `apps/docs/tests/`.
- ## Build shape: dual ESM+CJS, and why the CJS bundles workspace deps
+ ## It is published
- Playwright's Babel `require()` hook transpiles workspace-realpath ESM to
- CJS while Node still evaluates the real `.mjs` as ESM — the require chain
- must stay CJS end-to-end. So `tsdown.config.ts` builds twice: ESM with all
- workspace deps external, CJS with `@zitadel/api` + `@zitadel/config`
- **bundled in** (`deps.alwaysBundle`; the deprecated `noExternal` silently
- loses to auto-external). `@zitadel/cli` stays external in both — it is a
- regular published dependency resolved at install time. If you add a
- workspace runtime dep, decide its side of that line explicitly.
+ - It is on the release train: in `PUBLIC_RELEASE_PACKAGES`
+ ([`scripts/release-manifest.mjs`](../../scripts/release-manifest.mjs)) and
+ the changesets `fixed` group, in the same order
+ (`apps/cli/tests/unit/scripts/release-manifest.test.ts`).
+ - Every observable change needs a changeset. The kit is shipped product: a
+ new capability is `feat` and a repair `fix`, never `test:`
+ ([`.changeset/README.md`](../../.changeset/README.md)).
+ - Releases land on the `alpha` dist-tag while changesets is in pre mode, and
+ `latest` stays at the `0.0.0` placeholder on purpose. Never "fix" or flag
+ it.
+ - Never edit versions or dist-tags or publish by hand: the train owns them.
+ - Keep `LICENSE`, `publishConfig.access` and the `license` field, and the
+ package in [`LICENSING.md`](../../LICENSING.md)'s MIT exceptions.
+ - `build-release` is part of `tools/release/moon.yml`'s
+ `build-public-packages`, which `release:check-graph` enforces.
- `@playwright/test` is an **optional peer** (`>=1.60`): only the
- `./playwright` entry may import it. Never let it leak into `./` — the
- vitest/backend surface must work without Playwright installed.
+ ## Build shape
- ## Behavior contracts locked by unit tests
+ - Playwright's `require()` hook needs a CJS chain end to end, so
+ `tsdown.config.ts` builds ESM with the workspace deps external and CJS with
+ `@zitadel/api` and `@zitadel/config` bundled (`deps.alwaysBundle`; the
+ deprecated `noExternal` silently loses to auto-external). `@zitadel/cli`
+ stays external in both. Decide a new workspace runtime dep's side
+ explicitly.
+ - `@playwright/test` is an optional peer: only the `./playwright` entry may
+ import it, so the root entry works without Playwright.
- These encode externally-proven behavior; the unit suites pin them so a
- refactor cannot drift silently. Change the test only when the external
- fact changes:
+ ## Behaviour pinned by unit tests
- - **Passkey CDP profile** (`src/passkey.ts`): the exact
- `WebAuthn.addVirtualAuthenticator` options are the journey-CI-proven
- profile (ctap2, internal, resident keys, user verification, automatic
- presence). Changing any flag changes which ceremonies complete. The
- authenticator is **page-bound** — registration and the later login must
- run on the same page; sign out via `context().clearCookies()`, never a
- fresh context. RP-ID rule: serve the app on HTTPS or `http://localhost`;
- raw IP origins are invalid.
- - **Flow ceremonies** (`src/flows.ts`): the locator ladders are the
- widget's documented automation-hook contract
- (`packages/components/README.md`), locked by descriptor-equality unit
- tests. Ceremonies branch only on **widget-observable state** (which
- fields/actions render), never on app state — customer flow configs
- legitimately vary. Broad fallbacks (accessible names, labels, bare
- `data-action`) stay scoped to the `<zitadel-login>` host: `.or()` unions
- resolve in page-wide DOM order, so an unscoped candidate could match app
- chrome; the host also exists for custom templates that emit no hooks.
- Free-form action names are escaped per the CSSOM string-serialization
- rules before attribute-selector interpolation (#715), so an exotic name
- cannot break the union's parsing.
- - **Session-mint driver** (`src/session.ts`): the flow is stateless via
- the sealed `_zflow` cookie (raw `fetch` + one-cookie jar — the typed
- client hides response headers), submits must send an `Origin` on the
- project allowlist, and `/sessions/me` authenticates via the session
- cookie, not bearer.
- - The flow-step shape itself is owned by
- [`packages/config/defaults/default-login.json`](../config/defaults/default-login.json).
- If a kit helper needs a shape the definition does not declare, the
- definition changes first (same rule as `packages/api-mock`).
+ Change these tests only when the external fact changes.
- ## Credential contract (root ADR 053 §9)
+ - `src/passkey.ts`: the virtual-authenticator options are the profile the
+ journey proved. The authenticator is page-bound: register and sign in on
+ the same page, and sign out with `context().clearCookies()`, never a fresh
+ context. Serve the app on HTTPS or `http://localhost`; an IP origin is not
+ a valid RP ID.
+ - `src/flows.ts`: the locator ladders are the widget's automation-hook
+ contract (`packages/components/README.md`). Branch only on what the widget
+ renders, never on app state; scope broad fallbacks to the `<zitadel-login>`
+ host; escape free-form action names before building a selector.
+ - `src/session.ts`: the flow is stateless through the sealed `_zflow` cookie
+ (raw `fetch` with a one-cookie jar), submits send an `Origin` on the
+ project's allowlist, and `/sessions/me` authenticates by cookie, not bearer.
+ - A helper that needs a step shape `packages/config/defaults/default-login.json`
+ does not declare changes the definition first.
- Predictable test/dev credentials are a property of **this kit**, never of the
- server or the production seed contract. The kit boots the process and applies
- seeds, so it captures credentials from provisioning output (`POST /projects`
- returns `project_secret` exactly once) and exposes them on `InstanceHandle`:
- `projectSecret` today; the `platform` slot (`PlatformCredentials` — reserved
- project id + publishable key, the pair the ADR itself guarantees) once the
- platform-project provisioner lands. Root
- [ADR 053 §9](../../docs/adrs/053-cross-project-principals.md) (Proposed)
- sets the rule: test infrastructure gets its credentials from the testkit's
- boot contract, not from seed defaults — paraphrased here on purpose; §9's
- exact sentence lives in the ADR, so a reword there doesn't strand this file.
- [Console ADR 0004 §2](../../apps/console/docs/adrs/0004-console-deployment-modes.md)
- names the testkit as the bootstrap transport test and dev infrastructure
- use. While the ADR remains Proposed, treat the slot as tracking it — the
- slot is inert, so a change there moves this shape, not shipped behavior.
+ ## Credentials
- Consequences for changes here:
+ Test credentials come from the kit's boot contract, never from the server
+ ([ADR 053 §9](../../docs/adrs/053-cross-project-principals.md),
+ [console ADR 0004 §2](../../apps/console/docs/adrs/0004-console-deployment-modes.md)).
- - Never add — or lean on — a server `--test-mode` flag, a seed-default
- credential, or any other server-side door that mints deterministic
- credentials. If the kit cannot capture a credential at provisioning time,
- extend the provisioning contract the boot path drives (Console ADR 0004 §2
- makes the testkit a transport of that seed input) — not the server
- surface, and not direct database writes: the kit has no storage boundary
- (it shells out to the CLI and provisions over HTTP), raw writes would
- bypass the full provisioner, and seed ops must stay meaningful for
- `connectZitadel` targets (see the Don't list).
- - New credentials extend `InstanceHandle` — the `platform` slot is the
- contract point; don't invent a parallel channel. The handle must stay
- serializable: it crosses process boundaries via the handshake file, and
- `AppEnvTemplate` deliberately reaches only its flat string fields.
- - Declare a credential field only when its design is settled: additions are
- churn-free minors, removals are not, so deferred credentials (the platform
- automation principal, a boot-minted operator session) join as optional
- fields when their ADRs land — never pre-frozen, and never with an invented
- wire prefix in code or fixtures. Serialize only non-derivable state: a
- session cookie is rebuilt from its token (`SESSION_COOKIE_NAME` +
- constants), so a future serialized session is `{ sessionToken, expiresAt }`,
- not a stored cookie object.
+ - Never add or lean on a server `--test-mode` flag, a seed-default
+ credential or a direct database write. A credential the kit cannot capture
+ at provisioning extends the provisioning contract instead.
+ - New credentials extend `InstanceHandle` (the `platform` slot is the
+ contract point). Keep it serializable: it crosses processes in the
+ handshake file.
+ - Add a credential field only once its design is settled, as an optional
+ field, never with an invented wire prefix. Serialize only state that cannot
+ be derived (a session is `{ sessionToken, expiresAt }`, not a cookie).
- Dev loops feed from boot output: `console:dev-real` uses
- `handle.projectSecret` server-side only, to seed and to grant the dev user
- access; the console itself signs in with that user's session cookie (#1300).
- Don't reintroduce developer-remembered credential env vars.
-
- ## Moon layering
-
- The layer is `tool`, not `library` — deliberately between the e2e suites
- (`automation`, which may not depend on `automation`) and the CLI (`tool`,
- which `library` may not depend on). Consumers add `dependsOn: testing` and
- a `testing:build` task dep.
-
- ## Validation lanes
-
- - `moon run testing:test testing:lint testing:typecheck` — unit gate.
- - `moon run testing:test-integration` — boots real instances
- (binary + SQLite local default).
- - Dogfood: `moon run e2e:demo-next-real`, `moon run e2e:console-real`,
- `moon run e2e:journey -- --ci` (runs the testkit variant among others).
-
- The real-instance lanes are dependencies of `e2e:all`, which
- `.github/workflows/ci.yml` runs in its own e2e step after the build-and-test
- graph, so moon still orders them through the DAG. They are `runInCI: "skip"`
- only to stay out of that graph, where they would compete with the unit suites;
- `:test` alone stays unit tests. Run one locally with `moon run e2e:<lane>`, or
- all of them with `moon run e2e:all`, side by side; the journey lane then needs
- prebuilt tarballs in `JOURNEY_TARBALLS_DIR` (`e2e/moon.yml` says why).
-
- Serialize moon invocations — two concurrent graphs produce spurious
- failures. Orphaned runtimes from aborted e2e runs squat fixed ports;
- `moon run workspace:cli -- stop --all` sweeps them.
+ `handle.projectSecret` server-side only. Never reintroduce remembered
+ credential env vars.
+ - Keep every seed op meaningful against a `connectZitadel` target, not only a
+ local instance, and let helpers do only what a customer's test could (no
+ `page.evaluate` shortcuts).
- ## Don't
+ ## Moon and the e2e lanes
- - Don't make seed ops instance-specific. Every seed op must stay
- meaningful for `connectZitadel` targets (remote dev instances), not just
- locally-booted ones — the README positions the local runtime as the
- shipped core and remote mode as roadmap, and seed ops must not foreclose
- that direction.
- - Don't publish-adjacent by hand: no version edits, no dist-tag changes,
- no manual `npm publish`. The train owns all of it (the one-time name
- bootstrap already happened).
- - Don't remove the `LICENSE` file or `publishConfig.access` — published
- sibling parity, checked in review not by tooling. The package is listed
- in the root [`LICENSING.md`](../../LICENSING.md) MIT exceptions; keep
- the `license` field, the `LICENSE` file, and that list in agreement.
- - Don't reach for `page.evaluate`-style shortcuts in helpers that model
- customer usage; helpers must only do what a customer's test could do
- with the public product surface.
+ - The moon layer is `tool`, between the e2e suites (`automation`) and the CLI
+ (`tool`). Consumers add `dependsOn: testing` and a `testing:build` task dep.
+ - Unit gate: `moon run testing:test testing:lint testing:typecheck`; real
+ instances: `moon run testing:test-integration`.
+ - `e2e:all` (`e2e/moon.yml`) depends on every lane CI runs,
+ `testing:test-integration` among them, and `ci.yml` runs it in its own e2e
+ step after the build-and-test graph. The lanes are `runInCI: "skip"` only to
+ stay out of that graph, so a plain `:test` stays unit tests.
+ - Run one lane with `moon run e2e:<lane>`, or all with `moon run e2e:all`;
+ the journey lane then needs prebuilt tarballs in `JOURNEY_TARBALLS_DIR`
+ (`e2e/moon.yml` says why).
+ - Serialize moon invocations: two concurrent graphs produce spurious
+ failures. `moon run workspace:cli -- stop --all` sweeps the runtimes an
+ aborted run left on fixed ports.