@xyo-network/dapp-kit-vitest-config
v2.2.0
Published
Vitest config preset for XL1 dApps — node/browser base plus opt-in local chain installers
Readme
@xyo-network/dapp-kit-vitest-config
Public Vitest preset for XL1 dApps. Sister to
@ariestools/vitest-config,
with opt-in projects that boot a published local XL1 chain so dApp authors
do not have to babysit xl1 start.
This is the publicly installable analog of the XYO-internal
@xyo-network/xl1-vitest-config apiLocal harness. It does not depend on
restricted @xyo-network/chain-test packages. The chain is the same public
xl1 CLI used by @xyo-network/dapp-kit-local.
A green local run proves dApp chain interactions. It is not Sequence or mainnet qualification.
Install
pnpm add -D @xyo-network/dapp-kit-vitest-config @ariestools/vitest-config vitest
# optional browser project
pnpm add -D @vitest/browser-playwright playwrightAn XL1 dApp already has @xyo-network/xl1-sdk and its peers; this package
declares them as peers. @xyo-network/xl1-cli arrives as a dependency and is
spawned by the local-xl1 setup. Do not import the CLI from application source.
Usage
Offline suite only
import { defineDappKitVitestConfig } from '@xyo-network/dapp-kit-vitest-config'
export default defineDappKitVitestConfig()Defaults match @ariestools/vitest-config: the node project is named node,
and a browser project (when enabled) re-runs shared specs. Swapping
defineXyVitestConfig for this helper is not a silent topology change.
Local XL1, opt-in
import { defineDappKitVitestConfig } from '@xyo-network/dapp-kit-vitest-config'
export default defineDappKitVitestConfig({
installers: { localXl1: { optInOnly: true } },
}){
"scripts": {
"test": "vitest run",
"test:local-xl1": "vitest run --project local-xl1"
}
}pnpm test stays offline. pnpm test:local-xl1 boots one published XL1 CLI
chain per spec file.
Every local installer (all remain opt-in)
export default defineDappKitVitestConfig({ installers: true })Installers
| Option id | Project name (--project) | Setup |
|-----------|----------------------------|--------|
| localXl1 | local-xl1 | Boots the published xl1 CLI (api + producer + finalizer) |
| apiLocal | api-local | Same published CLI chain as localXl1, for specs under spec/api-local |
| localEventPublisher | local-event-publisher | Boots its own XL1 chain and an Event Kit publisher fixture with signed HTTP subscriptions |
| localDatalake | local-datalake | Isolates specs; no auto-setup |
| localSystem | local-system | Isolates specs; serialized; 90s timeouts |
| localBrowser | local-browser | Isolates specs; serialized |
installers: true enables every id. Each catalog default is optInOnly: true:
include globs are stripped from the base node project, but the installer
project only registers when selected via --project <name>.
installers shapes
| Value | Effect |
|---|---|
| omitted / false | no installer projects |
| true | every id in DAPP_KIT_VITEST_DEFAULT_INSTALLERS |
| ['localXl1'] | just those ids, each with catalog defaults |
| { localXl1: true, localSystem: { hookTimeout: 120_000 } } | per-id enable / disable / override |
Per-installer overrides: env, hookTimeout, include, optInOnly,
setupFiles, test, testTimeout. An unknown installer id throws at config
load.
Resolution order for optInOnly, highest first: per-installer options →
top-level optInOnly map → catalog default.
export default defineDappKitVitestConfig({
installers: true,
// Always boot a chain on `pnpm test` (usually the wrong default)
optInOnly: { localXl1: false },
})Where specs must live
The local-xl1 project's include globs come from the catalog, not from
your include:
| Glob | Use |
|---|---|
| src/**/spec/local-xl1/**/*.spec.ts | single-package repo |
| src/e2e/spec/local-xl1/**/*.spec.ts | root-level e2e package |
| packages/**/src/**/spec/local-xl1/**/*.spec.ts | monorepo package specs |
A live spec outside these paths is not picked up by --project local-xl1, and
(having no matching exclude) still runs in the offline project — where no chain
exists.
Local chain with Event Kit publication
Enable this project when the dApp under test needs finalized-head wake delivery:
export default defineDappKitVitestConfig({
installers: { localEventPublisher: true },
})Put specs under src/**/spec/local-event-publisher/**/*.spec.ts (or
packages/**/src/**/spec/local-event-publisher/**/*.spec.ts), then run:
pnpm vitest run --project local-event-publisherThis project owns one fresh chain and publisher identity per spec file.
It includes the same rpcUrl, apiPort, and chainId globals as local-xl1;
do not add that installer's setup too. The publisher is available through
localEventPublisher() after setup's beforeAll hook. It accepts subscriptions
in process; it does not expose an unauthenticated registration HTTP endpoint.
After starting your dApp's admission endpoint, register it:
import { localEventPublisher } from '@xyo-network/dapp-kit-vitest-config'
// In your test or beforeAll, after starting your dApp:
const publisher = localEventPublisher()
// Configure your dApp's admission policy to authorize publisher.publisher.
const subscription = await publisher.subscribe({
endpoint: app.wakeUrl, // your local dApp's actual HTTP admission endpoint
audience: app.wakeAudience, // receiver-issued, exact audience
deploymentId: app.deploymentId,
lastProcessedPosition: app.fromPosition - 1,
maxPositionsPerWake: app.maxPositionsPerWake,
queueId: 'indexer', // omit when the receiver route has no queue ID
})
// Publication is automatic; no runOnce() or synthetic head advance is needed.
// Assert your dApp's observable result here, then stop before closing its server.
await subscription.stop()Each registration starts a real Event Kit WakePublisherActor. It reads the
chain's finalized head through the XL1 SDK, signs canonical Event Kit wake
Payloads with a fresh local account, and sends Authorization: Bearer <JWT>
plus the raw Payload to the webhook. No signing key is exposed to the dApp.
An every-block filter emits inclusive ranges, capped at 512 blocks by default;
wakes may coalesce several finalized blocks. The first cursor defaults to the
finalized head at registration, so only subsequent progress is published.
Set lastProcessedPosition to an earlier block to backfill, or -1 to include
genesis. A cursor ahead of finality is rejected. Configure the receiver with
authorizationChainId: publisher.chainId, source: { kind: 'xl1-finalized',
chainId: publisher.chainId }, and the same fromPosition and
maxPositionsPerWake. Derive its filter hash with Event Kit's
sourceWakeFilterHash and SOURCE_WAKE_FILTER_PROFILE, and bind the selected
subscription. No legacy block-range envelope or version selector is supported.
Subscriptions have independent cursors and outboxes. A failed or timed-out
delivery retries the same wake identity and content with a fresh JWT;
the cursor advances only after HTTP 200 or 202. The receiver must return
these statuses only after its own durable admission and handle duplicate wakes.
Redirects and non-loopback destinations are rejected. A slow or failing
subscriber does not block another subscriber.
subscription.status() reports the cursor, successful delivery count, pending
outbox, last failure, and stopped state. publisher.assertHealthy() reports
unresolved failures; automatic teardown checks health and stops every
subscription, the SDK session, and the chain even when a test fails. Stop a
subscription explicitly when intentionally testing rejection or before closing
its endpoint. stop() is idempotent. For custom timing, use
startLocalEventPublisher({ intervalMs: 100, requestTimeoutMs: 5000 }) in your
own setup and always call its stop() in teardown.
Scope: this is an ephemeral local test publisher. Cursor/outbox state lasts for the fixture lifetime, matching the chain's in-memory lifetime; it is not a process-restart durability fixture. The dApp owns its actual admission gate, grants, inbox, queue, and consumer processing. Registration does not fabricate Statement Graph grants or exercise the on-chain publisher/subscriber handshake.
Optional Event Kit packages
Only this project needs @ariestools/actor (^1.3),
@xyo-network/event-kit-actor (^0.1.4), and
@xyo-network/event-kit-schemas (^0.1.4). These are optional peers and the runtime
is loaded separately, so offline and chain-only consumers do not load them.
Install compatible Event Kit artifacts in the consuming project before selecting
this installer. The packed-consumer gate installs Event Kit 0.1.4 from npm in
a fresh isolated store and proves signed delivery without workspace source
links.
What the local-xl1 installer does
installLocalXl1Setup() registers a beforeAll / afterAll pair, so each
spec file gets its own chain:
- picks a port —
8080 + VITEST_WORKER_ID, falling back to an OS-assigned ephemeral port when that one is busy (it does not scan upward); - spawns the published
xl1CLI withapi,producer, andfinalizer, telemetry disabled, on loopback; - waits until a typed XL1 viewer can read
chainIdand the current head; - assigns test globals and logs
dapp-kit local XL1 started: API on <port>, chain <id>; - stops the child in
afterAll(dapp-kit local XL1 stopped).
The chain uses the well-known insecure mnemonic
test test test test test test test test test test test junk. Account 0 is
genesis-funded. Storage is in-memory and resets every spec file. The chain id
is ephemeral.
This is a dev chain: simplified consensus, no EVM staking layer. Treat a green run as "my chain interactions are correct," then validate against Sequence before shipping.
Test globals
| Global | Type | Contents |
|---|---|---|
| rpcUrl | string | http://127.0.0.1:<port>/rpc — use this |
| apiPort | number | the chosen port |
| chainId | string | fresh per boot |
Read globalThis.rpcUrl (or localXl1RpcUrl()); do not hardcode
http://localhost:8080/rpc. Only the first spec file lands on 8080 — a
parallel run boots additional chains on other ports.
Ambient types ship at @xyo-network/dapp-kit-vitest-config/globals:
{
"compilerOptions": {
"types": ["node", "@xyo-network/dapp-kit-vitest-config/globals"]
}
}Then build the gateway from the global:
import { assertEx } from '@ariestools/sdk'
import type { XyoViewer } from '@xyo-network/xl1-sdk'
import { GatewayBuilder } from '@xyo-network/xl1-sdk'
import { localXl1RpcUrl } from '@xyo-network/dapp-kit-vitest-config'
import {
beforeAll, describe, expect, it,
} from 'vitest'
let viewer: XyoViewer
beforeAll(async () => {
const gateway = await new GatewayBuilder()
.name('local')
.rpcUrl(localXl1RpcUrl())
.build()
viewer = assertEx(gateway.connection.viewer, () => 'local chain gateway exposed no viewer')
})
describe('finalized head (live local chain)', () => {
it('advances as the chain finalizes blocks', async () => {
const start = await viewer.finalization.headNumber()
let head = start
for (let attempt = 0; attempt < 40 && head <= start; attempt += 1) {
await new Promise(resolve => setTimeout(resolve, 250))
head = await viewer.finalization.headNumber()
}
expect(head).toBeGreaterThan(start)
})
})To sign, derive from LOCAL_XL1_DEV_MNEMONIC (account '0' is genesis-funded).
Running
pnpm test # offline: node (+ browser) projects only
pnpm vitest run --project local-xl1 # live: boots a chain per spec file
pnpm test:local-xl1 # the same, via the package scriptRepository self-qualification
The dapp-kit monorepo uses defineDappKitVitestConfig as its root configuration
owner and registers the public apiLocal installer (published XL1 CLI, not the
restricted @xyo-network/xl1-vitest-config package). The root imports this
package's source entry deliberately, so a clean checkout can load Vitest before
ignored dist/ output exists.
That source bootstrap is not treated as package evidence. The separate
pnpm test:packed-dapp-kit-vitest-config gate builds and packs this package,
installs the tarball into a temporary consumer with only declared dependencies,
type-checks a consumer-owned config, runs its offline project, and boots a local
XL1 chain through the packed local-xl1 setup export.
API
| Export | Description |
|--------|-------------|
| defineDappKitVitestConfig | Full root config (watch: false + projects + selection footer) |
| defineDappKitVitestProjects | Projects array only |
| defineDappKitInstallerProjects | Installer projects + base excludes + selection |
| installLocalXl1Setup / startLocalXl1Chain | Chain lifecycle used by the setup file |
| localXl1RpcUrl | Reads globalThis.rpcUrl or throws |
| localEventPublisher | Reads the per-spec publisher fixture or throws before setup |
| installLocalEventPublisherSetup / startLocalEventPublisher | Chain plus publisher lifecycle; the latter supports custom timing |
| LOCAL_XL1_DEV_MNEMONIC | Insecure local-dev mnemonic |
| formatProjectSelectionFooter / DappKitProjectSelectionReporter | Footer helpers |
| Re-exports from @ariestools/vitest-config | defineXyVitestConfig, defineXySerializedProject, … |
Disable the post-run project-selection footer with
projectSelectionFooter: false.
