Test an App
Use tablewalk/testing from server-side tests. The helpers work with Node’s test
runner, Vitest and Playwright; they do not modify your running App.
import { test } from 'node:test';import { createTestWorkspace, startTestApp, assertAuthorizationMatrix,} from 'tablewalk/testing';
test('reader access', async t => { const workspace = await createTestWorkspace(); t.after(() => workspace.close()); const app = await workspace.copy('./app', 'app'); const database = await workspace.database('business.db', db => { db.exec('CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT)'); // Run your migrations and deterministic seeds here. }); const host = await startTestApp({ workspace, directory: app, database }); await assertAuthorizationMatrix([ { name: 'ready', client: host.client(), path: '/readyz', status: 200 }, ]);});A workspace owns its temporary directory and registered cleanup functions. Use
workspace.own(() => service.close()) for dependencies, or await using for the
workspace. Cleanup runs in reverse registration order, continues after failures,
and reports them together. database always closes its SQLite handle, including
when migration or seed code throws. write creates text or JSON fixtures;
copy copies an authored App. Paths cannot escape the workspace.
startTestApp launches the production CLI in a separate process, waits for its
startup readiness signal, and stops it during workspace cleanup. It chooses an
OS-assigned port; authenticated hosts require a stable nonzero port, so startup
retries a reservation collision. config, args and environment allow the
same connection configuration, policy, commands and jobs used by deployment.
Inherited secrets are excluded; HOME and XDG configuration belong to the test.
Explicit App paths still mean what they say: change absolute database, auth and
job paths in a copied App to disposable paths before starting it.
startTestSuite accepts a suite configuration object, writes it under the
workspace and assigns its port. Relative paths resolve under that workspace.
Its grant({ appId, subjectId, roles, expectedRevision }) uses the production
shared-identity grant maintenance path. Keep suite environment mappings and
explicit test secrets in its environment. The suite retains production
capability checks; unsupported suite deployment features are not bypassed.
Independent accounts and authorization
Section titled “Independent accounts and authorization”Each host.client() has its own cookie jar. signUp, signIn, signOut use the
real email authentication endpoints; pass a custom endpoint path for mounted
Apps. request(path, { method, json, headers }) supplies the origin and cookies,
keeps redirects explicit and refuses another origin. It preserves cookie paths
so independent App sessions do not overwrite each other.
Use assertAuthorizationMatrix for anonymous, reader, writer, disabled account,
wrong tenant and wrong App cases. Every row names a client, path and expected
status (or statuses). request supports command bodies and headers; inspect
receives the response for assertions about hidden fields, row membership and
refusal codes. All rows run and failures retain the case name and underlying
assertion. Include a deliberate wrong expectation with assert.rejects once
to prove the negative checks really detect a defect.
Seed tenant memberships through the App’s maintenance API. App grants and tenant memberships are separate authorization boundaries. Check direct API/command requests as well as navigation, revocation, logout, webhook signatures and cross-tenant identifiers. A test-only HTTP client is not a complete browser cookie implementation; use Playwright for SameSite, browser origin and navigation tests.
Unit and browser tests
Section titled “Unit and browser tests”validateTestApp(directory) loads an authored App through the production loader
and authority-lock validation. It does not replace database-bound --check-app.
createTestClock(initial) provides now, date, advance and reset for your
own business functions. Inject this clock into business logic; production command
receipts, authorization and transactions should be tested through the HTTP host.
The repository’s examples/testing scaffold includes a Node test and a Playwright
fixture that own their host lifecycle, desktop/mobile projects, semantic locators,
and traces/screenshots on failure. No private tablewalk imports are needed. Extend
the fixture with your App’s sign-in, writes, validation and shared suite journey.
Run fast unit/API tests while editing, then browser projects before release:
node --import tsx --test tests/*.test.tsnpx playwright test --config playwright.config.tsIn CI install the pinned dependencies and browser once, run the tests without blind retries, report durations and retain the Playwright report and test-results folder on failure. Keep provider contract checks separate: a simulated provider cannot prove that a live provider still accepts your requests. An optional security scanner may target the disposable loopback host using test accounts; passing a scanner does not establish that your authorization policy is correct.
App test subprocesses and validation/account/grant maintenance subprocesses install a TCP guard before startup. Node fetch, http,
https and TCP database clients may connect to loopback by default; an attempted
other host fails the request and the test’s teardown, even if App code catches
the connection error. For a deliberate contract test, pass allowedHosts to the
host, or to validateTestApp(directory, { allowedHosts }). Unix sockets are denied. This is a test boundary for Node TCP clients, not
an operating-system sandbox: native addons, child processes, UDP and DNS require
an external network sandbox when those are part of your integration.
The same examples directory contains a real Keystone acceptance fixture. It
copies the Lender and Dealer Apps into an ESM workspace, selects their shared
menu, seeds a uniquely owned PostgreSQL database, and tests pending credit checks,
signed callbacks, durable deduplication, the resulting underwriting score and
cross-App denial. Its browser journey signs into the actual Lender App. Set
TABLEWALK_TEST_PG to a local database administrator URL and follow the example’s
README. host.account({ name, email, password }) provisions a shared identity
account explicitly for invite-only Apps; grants remain a separate operation.