dánwòdánwò

Project

Config

danwo.config.ts: defineConfig and every option.

danwo.config.ts
import { defineConfig, stripeEnvironment } from "@danwo/sdk"

import { launchShop, type Shop } from "./src/launch.ts"

const WEBHOOK_SECRET = "whsec_danwo_example"

export default defineConfig({
  testDir: "tests",
  workers: 2,
  timeout: 60_000,
  /*
   * After every action, wait until the shop's calls to Stripe have finished and play the webhooks
   * they caused on the emulator's clock, so tests never wait or settle by hand.
   */
  readiness: { mode: "page+backend", settle: true },
  /*
   * What `danwo test --changed` runs for a change to the shop's own code: the webhook handler
   * affects the tests that received Stripe's checkout webhook; the server's pages affect every
   * test that visited one.
   */
  impact: {
    rules: {
      "src/webhooks.ts": ["webhook checkout.session.completed"],
      "src/server.ts": ["/"],
      "src/launch.ts": ["/"],
    },
  },
  // One Stripe emulator, proxy and shop per worker; `danwo matrix` resets them between runs.
  async environment() {
    let shop: Shop | undefined
    const stripe = await stripeEnvironment({
      seed: () =>
        shop && {
          webhookEndpoints: [
            {
              url: `${shop.url}/webhooks/stripe`,
              events: ["checkout.session.completed"],
              secret: WEBHOOK_SECRET,
            },
          ],
        },
    })

    const start = async () => {
      shop = await launchShop(
        stripe.proxy.env({
          ...process.env,
          STRIPE_SECRET_KEY: "sk_test_danwo_example",
          STRIPE_WEBHOOK_SECRET: WEBHOOK_SECRET,
          SHOP_TEST_RESET: "1",
        }),
      )
      await stripe.reset()
    }

    await start()

    return {
      get baseUrl() {
        return shop?.url
      },
      proxy: stripe.proxy,
      webhooks: stripe.emulator.webhooks,
      fixtures: stripe.fixtures,
      settle: stripe.settle,
      async reset() {
        await shop?.stop()
        await start()
      },
      /*
       * `danwo matrix` resets once per worker, keeps this snapshot, and restores it before every
       * later run: the emulator's store comes back from memory and the shop empties its books,
       * so no run restarts the app.
       */
      snapshot: () => stripe.snapshot(),
      async restore() {
        await stripe.restore()
        await fetch(`${shop?.url ?? ""}/__test/reset`, { method: "POST" })
      },
      async close() {
        await shop?.stop()
        await stripe.close()
      },
    }
  },
})

danwo.config.ts default-exports defineConfig({...}); every key is optional.

defineConfig

function

defineConfig(config: DanwoConfig): DanwoConfig

Types a project's config; returns it unchanged.

DanwoConfig

interface

What danwo.config.ts default-exports; every key is optional.

MemberDescription
testDir?: stringWhere test files are found, relative to the config file; default the config's folder.
testMatch?: readonly string[]Globs relative to testDir; default every *.danwo.ts.
baseUrl?: stringThe app's URL when no environment provides one.
browser?: BrowserConfigThe browser every test runs in. contextOptions sets reduced motion, the colour scheme, the locale, the time zone, the viewport, HTTP credentials and headers sent with every request; they apply to every browser context the run opens, saved sessions' and the MCP server's included. A date-time time.set is given without an offset is read in timezoneId.
workers?: numberWorker processes; default 1. Each runs one browser attempt at a time.
splitFiles?: booleanHand workers single tests instead of whole files (default false), so one slow file does not hold a worker while the others sit idle. Only a file whose tests share nothing is split: no beforeAll or afterAll, and every test on isolation: "test"; other files run whole. Each test loads its file afresh, so tests must not pass values to each other in module variables.
retries?: numberExtra attempts for a failing test; default 0.
timeout?: numberOne test attempt's deadline in milliseconds; default 60 000.
model?: LanguageModelThe agent's model: an AI SDK model or a Vercel AI Gateway id.
judge?: LanguageModelThe model agent.assert judges with; another provider than model is best.
budget?: BudgetsLimits on what the agent spends, per step and per test: turns, model calls, dollars, wall time. Default 25 turns a step. A step or test that runs out fails with BUDGET_EXCEEDED.
judgeScreenshot?: booleanShow the judge of agent.assert a screenshot besides the page's text (default false).
vision?: VisionSettingWhen the agent may see the page as an image: auto (default) when the text view misses something (a canvas, an unnamed button, a frame it cannot list), always, or off. { mode, model } names a model of its own for reading screenshots.
cache?: { readonly dir: string; readonly replayOnly?: boolean }The replay cache; dir is relative to the config file. replayOnly: true makes every danwo test run as --replay-only: no model is called, and a step with no recording that replays fails with REPLAY_MISSING.
secrets?: Readonly<Record<string, SessionSecret>>Values the model never sees; tests pass secret(name).
files?: Readonly<Record<string, string>>Files tests may upload, by name; paths are relative to the config file.
relocate?: booleanWhether a replay looks for a recorded element that moved instead of handing the step back to the model (default true). danwo matrix never relocates: its runs follow recordings exactly.
relocation?: RelocationPolicyHow replays find recorded elements: similar-v1 (default) also takes the one element that clearly matches a moved one, exact never does. The policy is part of every recording's key, so changing it records the steps afresh. Takes precedence over relocate.
effects?: EffectsSettingWhether a replayed agent step must have the effects its recording had: the same elements appearing and vanishing on the page, the same provider calls and webhooks behind it (default true). { ignore: [...] } leaves text matching any pattern out of page effects. An act call can turn effects off for itself with { effects: false }.
readiness?: | ReadinessMode | { readonly mode?: ReadinessMode; readonly capMs?: number; readonly settle?: boolean }What each step waits for after an action. page+backend (the default with an environment that has a proxy): the page settles, then nothing the app started at the proxy or the webhook senders is still in flight. page: the page alone. off: no waiting. settle: true also lets the environment play out background work (webhooks on a manual clock) after every action, so a test needs no settle() of its own. capMs bounds one wait (default 5000).
appErrors?: { readonly console?: boolean }What counts as the app failing during a step, besides 5xx responses from the app and uncaught page errors: console: true (default false) adds console errors. A live agent step stops at the first with APP_ERROR, or PROVIDER_ERROR when a provider call failed before it; a step the app failed in is never kept as a recording.
sessions?: Readonly<Record<string, SessionDefinition<SessionArgs>>>Saved sessions by name: each is set up once per worker and its browser state reused by every test that names it (test.use({ session })), until the environment resets. The setup also receives the environment's fixtures.
environment?: (context: EnvironmentContext) => Promise<WorkerEnvironment>Starts what a worker's tests run against (app, emulators, proxy); called once per worker.
isolation?: IsolationHow much state tests share. test (default) starts every test, and every retry, from the environment's seeded state: emulators back to their seeds, the clock to now, the proxy's log and webhook queues empty, and data stores restored, with a file's beforeAll work and saved sessions kept on top. file resets once per file, none never. A file or describe block can choose its own with test.use({ isolation }). The app's own memory is reset only by the environment's restore or reset (e.g. by calling a test-reset endpoint).
record?: RecordMode | RecordOptionsThe flight recorder: always (default) keeps what every run did under .danwo/runs/<run>/ with an HTML report; on-failure keeps failed runs only; off records nothing. As an object, { mode, keepRuns } also says how many of the newest runs keep their recordings (default 50).
video?: VideoSettingVideo in the flight record, one per tab or the device's screen, with every step, call and webhook on its clock: on (default) keeps it with every record kept; retain-on-failure films every run and keeps the video of runs that did not pass (a matrix keeps failing and invalid trials'); off films nothing. Secret fields show as dots, but a secret the app prints as text is on screen: turn video off for suites that show one. As an object, { mode, cursor }; cursor: false films without the pointer drawn at each action.
reporters?: readonly Reporter[]Reporters told about every run besides the console, e.g. github() from @danwo/github.
impact?: ImpactConfigHow danwo test --changed maps the app's own source files to what tests touch: globs → app paths (/checkout), provider hosts, calls or webhooks. Test files and what they import need no rule; a change no rule or import explains selects every test.
lint?: LintConfig | falseIntegration lint over every run's provider calls and webhooks: per-rule levels by id ("stripe/idempotency-key": "error" | "warn" | "info" | "off") and which findings fail the run (failOn, default never). false turns it off.
matrix?: { /** Default `defaultOutboundFaults`: each provider's faults by host, plain HTTP failures elsewhere. */ readonly outboundFaults?: (call: OutboundCall) => readonly OutboundFault[] readonly webhookFaults?: readonly WebhookFault[] /** * Faults on the app's own data calls, through the environment's `data` proxies. Default * `defaultDataFaults`: writes lost before or after the server ran them, slow, or failing to * serialize; COMMITs lost or failing; BEGINs meeting a database that is down or full; cache * commands lost, slow, or answered OOM, READONLY or LOADING. */ readonly dataFaults?: DataFaultCatalogue /** Runs a faulted run again up to this many times while its fault never fires (default 0). */ readonly rerunInvalid?: number /** Worker processes for `danwo matrix`; default `workers`, else one per core (at most 8). */ readonly workers?: number /** * How long a faulted run's checks wait: at most `factor` times (default 3) as long as the same * check took in the clean run, and at least `floorMs` (default 1000; 6000 on a device, where * pacing starts once the trial's fault has fired). `false` keeps every check's normal timeout. */ readonly checkPace?: false | { readonly factor?: number; readonly floorMs?: number } /** * Race faults, each planned only where the clean run allows it; all on by default, `false` * turns every one off. `doubleSubmit`: each change-making request the page sends the app, * sent twice at once. `beforeRedirect`: a provider's webhooks delivered before the browser * lands back on the app. `webhookOrder`: two webhooks in a row delivered the other way round. * `slowThenRetry`: a payment provider's call held up to `ms` (default 3000) or until the app * gives up and retries, then processed anyway. Running a test by several people at once is * the test's own `concurrent` option. */ readonly races?: false | RaceSettings /** * Most faulted trials a matrix runs (default 40). Past it the plan folds repeated routes and * statements into their first, keeps each target's most telling faults, then keeps races, * webhooks and writes ahead of reads. `--budget` and `--all` override it. */ readonly budget?: number }Fault catalogue for danwo matrix.

Budgets

interface

A step's and a test's budget.

MemberDescription
step?: Budget
test?: Budget

RelocationPolicy

type

"exact" | "similar-v1"

The matching policies, by name.

ReadinessMode

type

"page" | "page+backend" | "off"

What is waited for after an action: the page, the page and the backend, or nothing.

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI