Testing & verification
In this chapter the app gains its production posture — a typed session, a guard chain, an order
history — and then proves its entire behavior surface without running a browser: kovo check over
the app graph, kovo explain as a queryable dependency graph, and harness tests that verify
observed writes against declared touches. Step state:
site/tutorial/steps/07-verification/.
This chapter has two halves. Guards (sessions, the guard chain, the new domain) lock down who
can do what. Verification (the app graph, kovo check, kovo explain, write verification)
proves the whole behavior surface mechanically. The mechanics of how this tutorial keeps itself
honest against the reference app — run-steps gating and parity — live in the
wrap-up; this chapter shows the assertion and moves on.
Guards: lock down who can do what#
Add a typed session and a guard chain#
req.session is a declared schema, not an any bag. Guard refinements and the order's userId
rest on typed fields; an untyped session would be a hole directly under the proof surface:
// SPEC.md section 6.5: the session is a declared schema, not an any-bag —
// guard refinements and the order's userId rest on typed fields.
export const shopSession = session(
s.object({
id: s.string(),
user: s.object({
id: s.string(),
}),
}),
);export interface ShopRequest {
db: ShopDb;
session?: { id?: string; user?: { id: string } | null } | null;
}The production path. The tutorial keeps a plain in-memory store and a hand-declared touch set. With
@kovojs/drizzle, the per-request database is real, touches are extracted from the write ASTs, and write verification runs against actual table writes. The data-layer guide is the home for the production story.
The mutation now runs behind authed plus a rate limit, writes a third domain (order), and
keeps everything else from chapter 5 — schema, errors, CSRF, declared touches:
export const addToCart = mutation('cart/add', {
csrf: shopCsrf,
input: s.object({
productId: s.string(),
quantity: s.number().int().min(1).default(1),
}),
errors: {
OUT_OF_STOCK: s.object({ availableQuantity: s.number().int().min(0) }),
},
guard: guards.all(
guards.authed<ShopRequest>(),
guards.rateLimit<ShopRequest>({ max: 10, per: 'session' }),
),
registry: {
inferredTouches: addToCartTouches,
queries: [cartQuery, productsQuery, orderHistoryQuery],
},
transaction(request: ShopRequest, run) {
return request.db.transaction((db) => run({ ...request, db }));
},
handler(input, request: ShopRequest, context) {
const currentSession = shopSession.parse(request);
const found = request.db.products.get(input.productId);
if (!found || found.stock < input.quantity) {
return context.fail('OUT_OF_STOCK', { availableQuantity: found?.stock ?? 0 });
}
request.db.write('cart_items', {
productId: input.productId,
qty: input.quantity,
unitPrice: found.unitPrice,
});
request.db.write('orders', {
id: `order-${request.db.orders.length + 1}`,
productId: input.productId,
qty: input.quantity,
total: found.unitPrice * input.quantity,
userId: currentSession.user.id,
});
request.db.write('products', {
...found,
stock: found.stock - input.quantity,
});
return { productId: input.productId, quantity: input.quantity };
},
});An order history island consumes the new domain. What had to change for it to participate:
nothing. It declares queries: { orderHistory }, and every cart mutation ever written updates
it, because optimism and invalidation are keyed to queries, not call sites:
export const OrderHistory = component({
queries: { orderHistory: orderHistoryQuery },
render: ({ orderHistory }: { orderHistory: OrderHistoryResult }) => (
<ol>
{orderHistory.items.map((item) => (
<li kovo-key={item.id}>
{item.productId} x {item.qty} - {item.total}
</li>
))}
</ol>
),
});Verification: prove the behavior surface#
Check the app graph#
Everything the app has declared — components and their queries, the mutation's guards and writes,
optimistic statuses, the page's query set, the touch graph (write sites mapped to touched
domains) — composes into one value. examples/commerce commits this as a generated artifact so
graph changes appear as diffs in code review; the tutorial declares it inline:
// The app graph: every fact kovo check and kovo explain reason over. In the
// blessed @kovojs/drizzle path most of this is derived (SPEC.md section 11.1);
// examples/commerce commits it as a generated artifact. Declared or derived,
// it is the same machine-checkable shape (section 11.4).
export const shopGraph = {
components: [
{ fragments: ['cart-badge'], name: 'CartBadge', queries: ['cart'] },
{ fragments: ['product-list'], name: 'ProductList', queries: ['products'] },
{ fragments: ['order-history'], name: 'OrderHistory', queries: ['orderHistory'] },
],
mutations: [
{
guards: ['authed', 'rateLimit:session'],
invalidates: ['cart', 'product', 'order'],
inputFields: ['productId', 'quantity'],
key: 'cart/add',
session: 'shopSession',
writes: ['cart', 'product', 'order'],
},
],
optimistic: [
{ mutation: 'cart/add', query: 'cart', status: 'hand-written' },
{ mutation: 'cart/add', query: 'products', status: 'await-fragment' },
{ mutation: 'cart/add', query: 'orderHistory', status: 'await-fragment' },
],
pages: [
{
modulepreloads: [],
prefetch: false,
queries: ['cart', 'products', 'orderHistory'],
route: '/',
stylesheets: [],
},
],
queries: [
{ domains: ['cart'], query: 'cart' },
{ domains: ['product'], query: 'products' },
{ domains: ['order'], query: 'orderHistory' },
],
touchGraph: shopTouchGraph,
} as const;kovo check is the CI gate over that graph — touch-graph consistency and optimistic exhaustiveness
in one stable, diffable output:
it('passes kovo check with no unhandled optimistic pair', () => {
expect(kovoCheck(shopGraph)).toEqual({
exitCode: 0,
output: 'kovo-check/v1\nOK\n',
});
});Query the graph with kovo explain#
kovo explain prints the compiler's and data plane's decisions as stable text, so agents consume
the same artifact humans read. The step pins the cart/add explanation, including the optimistic
status of every invalidated query:
it('explains the cart/add mutation as a stable, diffable artifact', () => {
const explanation = kovoExplain(shopGraph, {
kind: 'mutation',
optimistic: true,
target: 'cart/add',
});
expect(explanation.exitCode).toBe(0);
expect(explainLine(explanation.output, 'writes: ')).toBe('cart,product,order');
expect(explainLine(explanation.output, 'invalidates: ')).toBe('cart,product,order');
expect(optimisticStatuses(explanation.output)).toEqual(
new Map([
['cart', 'hand-written'],
['products', 'await-fragment'],
['orderHistory', 'await-fragment'],
]),
);
expect(explainLine(explanation.output, 'OPTIMISTIC-SUMMARY ')).toContain('UNHANDLED=0');
});Because the output is stable, intent-level questions become set operations over printed graphs. Here is the acceptance question — "what updates when cart/add commits?" — answered mechanically:
it('answers "what updates when cart/add commits" mechanically', () => {
const mutationExplain = kovoExplain(shopGraph, { kind: 'mutation', target: 'cart/add' });
const pageExplain = kovoExplain(shopGraph, { kind: 'page', target: '/' });
const pageQueries = explainList(explainLine(pageExplain.output, 'queries: '));
expect(pageQueries).toEqual(['cart', 'products', 'orderHistory']);
// Set operations over printed graphs: every query this page renders is
// updated by cart/add, and each names its consuming component.
const updates = explainLine(mutationExplain.output, 'updates: ');
for (const query of pageQueries) {
const queryExplain = kovoExplain(shopGraph, { kind: 'query', target: query });
const consumers = explainList(explainLine(queryExplain.output, 'consumers: '));
expect(updates).toContain(`${query}->`);
expect(consumers.some((consumer) => consumer.startsWith('component:'))).toBe(true);
expect(explainList(explainLine(queryExplain.output, 'invalidated-by: '))).toContain(
'cart/add',
);
}
});The unguarded audit rides the same surface: kovo explain --unguarded lists every mutation, route,
and query reachable without authed. This app's answer is zero.
Verify writes in harness tests#
@kovojs/test executes mutations as functions, with write verification on: every observed write
must fall inside the declared touch set, or the test fails. If the handler someday writes a table
the touches don't declare, this test goes red before any user sees a stale page:
it('executes addToCart through the harness with write verification on', async () => {
const shopDb = createShopDb();
const harness = createKovoTestHarness({
db: shopDb,
pages: {
'/': () => renderShopPage(shopDb),
},
request: {
session: { id: 's1', user: { id: 'u1' } },
},
touchGraph: shopTouchGraph as unknown as ShopTouchGraph,
verification: {
domainByTable: {
cart_items: 'cart',
orders: 'order',
products: 'product',
},
},
});
// The verifier observes writes through the wrapped handle, so the test
// runs the handler against it directly instead of a cloned transaction
// draft (the examples/commerce acceptance-test pattern).
const verifiedDb = harness.dbHandle();
verifiedDb.transaction = (run) => run(verifiedDb);
const request = { db: verifiedDb, session: { id: 's1', user: { id: 'u1' } } };
await expect(
harness.exec(addToCart, formInput(request, { productId: 'p1', quantity: '2' }), {
touchGraphKey: 'cart.addItem',
}),
).resolves.toMatchObject({
changes: [
{ domain: 'cart', input: { productId: 'p1', quantity: 2 } },
{ domain: 'order', input: { productId: 'p1', quantity: 2 } },
{ domain: 'product', input: { productId: 'p1', quantity: 2 }, keys: ['p1'] },
],
ok: true,
rerunQueries: ['cart', 'products', 'orderHistory'],
});
// Observed writes ⊆ declared touches — the SPEC.md §11.2 invariant.
expect(harness.verificationDiagnostics()).toEqual([]);
await expect(
harness
.page('/')
.then((page: { fragment(target: string): string }) => page.fragment('cart-badge')),
).resolves.toContain('data-bind="cart.count"');
});Because application wiring is proof-carrying, the app needs few or no browser tests of its own. The framework keeps morph survival and L0 behaviors under its own browser suites; what it removes is the testing SPAs need to compensate for unverifiable wiring.
Assert parity with the reference app#
The last verification step pins this app to the reference: examples/commerce is the acceptance
target, and this step asserts behavior parity with its committed graph artifact — same mutation key
and named POST, same input fields and write set, same optimistic statuses per pair, same fragment
wire and failure code:
it('matches the reference commerce app: wire vocabulary and optimistic statuses', async () => {
// The committed graph artifact of examples/commerce — the rules/v1-acceptance.md
// acceptance target this tutorial has been building toward.
interface TutorialGraphComparison {
mutations: Array<{ inputFields: string[]; key: string; writes: string[] }>;
optimistic: Array<{ mutation: string; query: string; status: string }>;
}
const compareStrings = (left: string, right: string) => left.localeCompare(right);
const commerceGraph = JSON.parse(
readFileSync(
new URL('../../../../../examples/commerce/src/generated/graph.json', import.meta.url),
'utf8',
),
) as TutorialGraphComparison;
const commerceCartAdd = commerceGraph.mutations.find((entry) => entry.key === 'cart/add');
const shopCartAdd = shopGraph.mutations.find((entry) => entry.key === 'cart/add');
// Same mutation key — and therefore the same named POST: /_m/cart/add.
expect(addToCart.key).toBe('cart/add');
expect(renderShopPage()).toContain('action="/_m/cart/add"');
expect(renderShopPage()).toContain('kovo-fragment-target="add-to-cart:p1"');
// Same input field vocabulary and write set.
expect(shopCartAdd?.inputFields).toEqual(commerceCartAdd?.inputFields);
expect([...(shopCartAdd?.writes ?? [])].sort(compareStrings)).toEqual(
[...(commerceCartAdd?.writes ?? [])].sort(compareStrings),
);
// Same optimistic COVERAGE per pair (the list query is named productGrid in
// commerce, products here). The tutorial teaches v1 hand-written/await-fragment
// optimism; the reference commerce app has since adopted v2 derived optimism
// (SPEC.md §10.5). Both cover exactly the same (mutation × query) pairs with an
// explicit, non-UNHANDLED status — that coverage parity is the invariant here,
// not the v1-vs-v2 status string.
const queryNameMap: Record<string, string> = {
cart: 'cart',
orderHistory: 'orderHistory',
products: 'productGrid',
};
const pairKey = (entry: { mutation: string; query: string }) =>
`${entry.mutation} ${entry.query}`;
const shopPairs = shopGraph.optimistic.map((entry) =>
pairKey({ mutation: entry.mutation, query: queryNameMap[entry.query] ?? entry.query }),
);
const commercePairs = commerceGraph.optimistic
.filter((entry) => entry.mutation === 'cart/add')
.map(pairKey);
// Both apps cover exactly the same three cart/add (mutation × query) pairs.
expect([...shopPairs].sort(compareStrings)).toEqual([...commercePairs].sort(compareStrings));
expect(shopPairs).toHaveLength(3);
// No pair is UNHANDLED on either side (commerce derived, shop hand-written/await).
expect(commerceGraph.optimistic.every((entry) => entry.status !== 'UNHANDLED')).toBe(true);
// Same enhanced wire: kovo-query truth plus fragments, same failure code.
const request = shopRequest();
const success = await submitAddToCart(
formInput(request, { productId: 'p1', quantity: '2' }),
request,
{
'Kovo-Fragment': 'true',
'Kovo-Live-Targets':
'cart-badge#components/cart-badge/cart-badge:{}; product-list#components/product-list/product-list:{}; order-history#components/order-history/order-history:{}',
'Kovo-Targets': 'cart-badge=cart; product-list=products; order-history=orderHistory',
},
);
expect(success.headers['Content-Type']).toBe('text/vnd.kovo.fragment+html; charset=utf-8');
expect(success.body).toContain('<kovo-query name="cart">{"count":2}</kovo-query>');
expect(success.body).toContain('<kovo-fragment target="order-history">');
expect(success.body).toContain('kovo-key="order-1"');
expect(success.headers['Kovo-Changes']).toBe(
'[{"domain":"cart"},{"domain":"order"},{"domain":"product","keys":["p1"]}]',
);
const failure = await submitAddToCart(
formInput(request, { productId: 'p2', quantity: '3' }),
request,
{
'Kovo-Form-Target': 'add-to-cart:p2',
'Kovo-Fragment': 'true',
'Kovo-Targets': 'add-to-cart:p2',
},
);
expect(failure.status).toBe(422);
expect(failure.body).toContain('data-error-code="OUT_OF_STOCK"');
});How this parity (and every code block in the tutorial) stays true in CI — the run-steps gate
and what it enforces — is explained in the wrap-up.
The testing guide covers pglite-backed harnesses and HTTP-level assertions; the
kovo explain guide tours the full command surface.
Guarded, session-typed, and provable without a browser. One short chapter remains: shipping it.
Spec & diagnostics
Behavior surface proven without a browser: SPEC §11.4. Typed session schema: SPEC §6.5. Optimism
and invalidation keyed to queries, not call sites: SPEC §10.4. App graph as one composed value:
SPEC §11.4. kovo check exhaustiveness and consistency gate: SPEC §10.6. kovo explain stable text
for humans and agents: SPEC §5.3. Acceptance intent question: rules/v1-acceptance.md. Unguarded audit and
zero unauthed reach: SPEC §10.3. observed ⊆ static write-verification invariant: SPEC §11.2.
Reference-app parity: rules/v1-acceptance.md.