AGENTS.md@apps/cli/tests · git:20261020.8d47d7a · 2026-10-20 · sha256 d48e6450a67380d3

AGENTS.md@apps/cli/tests git:20261020.8d47d7aA

Immutable. This exact content never changes and is served at /api/v1/blob/d48e6450a67380d3.

# Agent Instructions — `apps/cli/tests`

Package rules: [`../AGENTS.md`](../AGENTS.md).

## Two kinds of test

Every file here is a unit test or a spec. A test that is neither gets
reclassified until it is one.

- **Unit tests** (`tests/unit/**/*.test.ts`) import from `src/` (or read a
  checked-in file) and mirror its layout: `src/lib/idp/credentials.ts` →
  `tests/unit/lib/idp/credentials.test.ts`. One file may cover a cohesive
  directory if the mirror path stays obvious. Doubles are fine.
- Unit tests own all detail: wording, key order, classification, renderer
  output, validation messages, id prefixes, envelope fields. A spec never
  asserts what a unit test can, so a failure names the module that broke.
- **Specs** (`tests/integration/**/*.spec.ts`) drive the built CLI through
  `runCliForTest` against `@zitadel/api-mock`'s platform handlers and assert
  only what a developer could observe.
- One spec file per command, named after it (`variables-set.spec.ts`), the
  only spec that drives that command. No cross-command, "contract" or
  "round-trip" files.
- A property every command must have lives in the fixture: `ScaffoldedApp`
  checks the envelope contract on every `--json` run.
- The binary's own behaviour (unknown command, server resolution) is
  `cli.spec.ts`. The generated resource commands share one factory
  (`src/lib/oclif/crud/`): unit-test the factory, and `resources.spec.ts`
  proves it is wired.

## The shape of a spec

```ts
const platform = usePlatformMock();

describe("auth-method sso enable", () => {
  describe("against an invalid server", () => {
    describe("that is not a zitadel api", () => {
      it("still writes the provider into the project", async () => {
        const app = await aSetUpApp();
        platform.isNotZitadel();

        const result = await app.enableSso("google", CREDENTIALS);

        expect(result).toSucceed();
      });
    });

    describe("that is down", () => { … });
  });

  describe("against a valid server", () => {
    describe("--json", () => {
      it("registers nothing with the platform until apply", async () => {
        const app = await aSetUpApp();

        await app.enableSso("google", CREDENTIALS);

        expect(await app.registeredIdps()).toEqual([]);
      });
    });

    describe("rendered for a terminal", () => { … });
  });
});
```

- **Two top-level groups, in that order.** The invalid-server group states
  what the command does against a server that is not a Zitadel API and one
  that is down, including when it carries on regardless. The `--json` and
  rendered groups never interleave; drop the second if nothing is rendered.
- **Arrange with flags, act on your own command.** Reach the starting state
  through other commands, non-interactively and unasserted. `setup.spec.ts`
  alone has the wizard as its subject.
- **Observe by running a command** (`idps list`, `plan`, …, through helpers
  like `app.registeredIdps()`), never by reading `.zitadel/state.json` or the
  mock's store.
- Read a committed file (`app.committed`) only where its content is the
  behaviour: a credential is a `${{ VARIABLE }}` reference, never a literal.
- **One journey and one claim per test**: arrange, blank line, one command,
  blank line, assertions. Use `it.each` for what varies by framework, preset
  or resource.
- **No paths**: edit and arrange through `app.edit*`, `app.addFlow`,
  `app.writeConfig`, `app.writeLocalSecret`.
- **No HTTP**: arrange the platform in its own terms (`platform.recovers()`,
  …); msw stays in `helpers/platform.ts`.
- **Only the matchers in `helpers/matchers.ts`**, never a bare `exitCode`
  compare. Assert rendered output with `toPrint` (stdout alone), not `toSay`,
  which spans both channels.
- **No comments** (one exception below): a step that needs one becomes a
  named helper with TSDoc.
- `cli.spec.ts` alone may read `src/**`: it checks the commands the CLI
  suggests against the command list the built CLI reports.

## Prohibited in specs

- No doubles of our own modules. Fake an external binary on `PATH` if needed.
- Never a real Zitadel. If the mock cannot express a behaviour, teach the mock
  first.
- Where the mock accepts what the real engine refuses, a comment names the
  server-side validator the spec stands in for.

## Ports and network

- A port handed to the CLI comes from `nextPort()`
  ([`helpers/ports.ts`](helpers/ports.ts)), never a bind-and-close probe:
  another worker's outbound connection can take it first (`E_PORT_IN_USE`).
  Never retry on that. A listener held for the whole test may bind port 0.
- No test leaves the machine: [`helpers/offline.ts`](helpers/offline.ts) fails
  any connection off loopback.
- A unit test answers the platform with
  `setupServer(...setupPlatformHandlers())` and
  `onUnhandledRequest: "error"`; a spec with `usePlatformMock()`. A command
  without `--server` talks to `DEFAULT_SERVER`, the real platform, so it needs
  the same.