---
title: '7. Testing & verification'
description: Guards, the app graph, kovo check and kovo explain, harness tests with write verification — and parity with the reference app.
order: 7
---

# Testing & verification

In this chapter the app gains its production posture — a typed session, a guard chain, and a
principal-scoped cart and order history — 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](/tutorial/08-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:

```tsx
// SPEC.md section 6.5: the session is a declared schema, not an any-bag —
// guard refinements and the cart/order userId fields rest on typed fields.
export const shopSession = session(
  s.object({
    id: s.string(),
    user: s.object({
      id: s.string(),
    }),
  }),
);
```

```ts
export interface ShopRequest extends Request, TaskSchedulingRequest {
  db: ShopDb;
  env: Readonly<Record<never, never>>;
  session: { id?: string; user?: { id: string } | null } | null;
  tutorialDbToken: object;
}
```

> **The production path.** The tutorial keeps a plain in-memory store and a hand-declared touch
> set. With [`@kovojs/drizzle`](/guides/data-layer/), 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](/guides/data-layer/) is the home for the production story.

The mutation now declares the canonical `access` guard chain — `authed` plus a rate limit — writes
a third domain (`order`), and keeps everything else from chapter 5: schema, errors, CSRF, and
declared touches:

```tsx
export const addToCart = app.mutation({
  input: addToCartInput,
  errors: {
    OUT_OF_STOCK: s.object({ availableQuantity: s.number().int().min(0) }),
  },
  access: [app.all(app.authenticated, app.rateLimit({ max: 10, per: 'session' }))],
  registry: {
    queries: [cartQuery, productsQuery, orderHistoryQuery],
    touches: [cart, order, product],
  },
  queue: 'cart',
  optimistic: [
    cartQuery.optimistic(addToCartInput, predictCart),
    orderHistoryQuery.optimistic('await-fragment'),
    productsQuery.optimistic('await-fragment'),
  ],
  transaction<Result>(
    request: ShopRequest,
    run: (transactionRequest: ShopRequest) => Promise<Result>,
  ): Promise<Result> {
    return tutorialShopDb(request).transaction((db) =>
      run(Object.assign(request.clone(), request, { db })),
    );
  },
  handler(input, request, 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.cartItems.push({
      productId: input.productId,
      qty: input.quantity,
      unitPrice: found.unitPrice,
      userId: currentSession.user.id,
    });
    request.db.orders.push({
      id: `order-${request.db.orders.length + 1}`,
      productId: input.productId,
      qty: input.quantity,
      total: found.unitPrice * input.quantity,
      userId: currentSession.user.id,
    });
    request.db.products.set(input.productId, { ...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:

```tsx
export const OrderHistory = component({
  queries: { orderHistory: orderHistoryQuery },
  render: ({ orderHistory }: { orderHistory: OrderHistoryResult }) => (
    <ol>
      {orderHistory.items.map((item) => (
        <li key={item.id}>
          {item.productId} x {item.qty} - {item.total}
        </li>
      ))}
    </ol>
  ),
});
```

The cart becomes private in this final production-posture step. Each row carries its owner, and its
canonical `access: [guards.authed()]` chain and loader use the same typed session principal. Earlier
tutorial steps deliberately model one anonymous cart and label that simplification as public; step
7 does not:

```ts
export function loadCart(db: ShopReadModel, userId: string): CartResult {
  return {
    count: db.cartItems
      .filter((item) => item.userId === userId)
      .reduce((total, item) => total + item.qty, 0),
  };
}

export const cartQuery = app.query({
  access: [app.authenticated],
  load: (_input, context) =>
    loadCart(tutorialShopDb(context.request).query.snapshot(), requireShopUserId(context)),
  reads: [cart],
});
```

Order history follows the same rule. Its loader filters `userId` instead of trusting the guard to
imply row ownership:

```ts
export function loadOrderHistory(db: ShopReadModel, userId: string): OrderHistoryResult {
  return { items: db.orders.filter((item) => item.userId === userId) };
}

export const orderHistoryQuery = app.query({
  access: [app.authenticated],
  load: (_input, context) =>
    loadOrderHistory(tutorialShopDb(context.request).query.snapshot(), requireShopUserId(context)),
  reads: [order],
});
```

The victim/attacker regression exercises both boundaries and proves an authenticated attacker sees
only their own cart count and order rows, while anonymous requests are rejected before either
loader succeeds:

```ts
it('guards and scopes cart and order history to the authenticated principal', async () => {
  const db = createShopDb();
  db.cartItems.push(
    {
      productId: 'p1',
      qty: 4,
      unitPrice: 1499,
      userId: 'victim',
    },
    {
      productId: 'p2',
      qty: 1,
      unitPrice: 2599,
      userId: 'attacker',
    },
  );
  db.orders.push(
    {
      id: 'order-victim',
      productId: 'p1',
      qty: 1,
      total: 1499,
      userId: 'victim',
    },
    {
      id: 'order-attacker',
      productId: 'p2',
      qty: 1,
      total: 2599,
      userId: 'attacker',
    },
  );

  const attackerRequest = createShopRequest(db, {
    id: 's-attacker',
    user: { id: 'attacker' },
  });
  const attackerPage = await renderShopPageForTest(attackerRequest);
  expect(attackerPage).toContain('<cart-badge');
  expect(attackerPage).toContain('kovo-key="order-attacker"');
  expect(attackerPage).not.toContain('order-victim');

  const anonymousPage = await renderShopPageForTest(
    createShopRequest(db, { id: 's-anonymous', user: null }),
  );
  expect(anonymousPage).toContain('Pour-over kettle');
  expect(anonymousPage).not.toContain('<cart-badge');
  expect(anonymousPage).not.toContain('order-attacker');
});
```

## 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),
and the framework-produced access ledger — composes into one value. `examples/commerce` commits
this as a generated artifact so graph changes appear as diffs in code review; the tutorial keeps
the behavior fixture inline and derives its access facts from the actual route/query/mutation
declarations:

```tsx
// 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: addToCart.key,
      session: 'shopSession',
      writes: ['cart', 'product', 'order'],
    },
  ],
  optimistic: [
    { mutation: addToCart.key, query: 'cart', status: 'hand-written' },
    { mutation: addToCart.key, query: 'products', status: 'await-fragment' },
    { mutation: addToCart.key, 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:

```ts
it('passes kovo check with no unhandled optimistic pair', () => {
  expect(kovoCheck(verifiedShopGraph)).toEqual({
    exitCode: 0,
    output: [
      'kovo-check/v1',
      `COVERAGE component=CartBadge query=${cartQuery.key} position="text" status=plan`,
      '',
    ].join('\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:

```ts
it('explains the addToCart mutation as a stable, diffable artifact', () => {
  const explanation = kovoExplain(verifiedShopGraph, {
    view: 'mutation',
    optimistic: true,
    target: addToCart.key,
  });

  expect(explanation.exitCode).toBe(0);
  expect(explainLine(explanation.output, 'writes: ')).toBe('cart,product,order');
  expect(explainLine(explanation.output, 'invalidates: ')).toBe(
    `${cartQuery.key},${productsQuery.key},${orderHistoryQuery.key}`,
  );
  expect(optimisticStatuses(explanation.output)).toEqual(
    new Map([
      [cartQuery.key, 'hand-written'],
      [productsQuery.key, 'await-fragment'],
      [orderHistoryQuery.key, '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:

```ts
it('answers "what updates when addToCart commits" mechanically', () => {
  const mutationExplain = kovoExplain(verifiedShopGraph, {
    view: 'mutation',
    target: addToCart.key,
  });
  const pageExplain = kovoExplain(verifiedShopGraph, { view: 'page', target: '/' });
  const pageQueries = explainList(explainLine(pageExplain.output, 'queries: '));

  expect(pageQueries).toEqual([cartQuery.key, productsQuery.key, orderHistoryQuery.key]);

  // Set operations over printed graphs: every query this page renders is
  // updated by addToCart, and each names its consuming component.
  const updates = explainLine(mutationExplain.output, 'updates: ');
  for (const query of pageQueries) {
    const queryExplain = kovoExplain(verifiedShopGraph, { view: '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(
      addToCart.key,
    );
  }
});
```

The access audit rides the same surface. The product catalog remains visibly `public`; the cart
query, order-history query, and cart mutation remain visibly guarded. `kovo explain unguarded`
reports zero only because no route, query, or mutation is missing a decision — it does not relabel
public surfaces as authenticated.

### Verify writes against the built app

The public `@kovojs/test` harness binds an imported app contract to an explicit successful-build
artifact. Mutation, query, route, request, and DB types come from the app; touch/read facts come
from the digest-verified artifact. If source changes after the build or a handler writes outside
the proved graph, the test fails before a user sees stale data. The runnable pattern lives in
[Testing with @kovojs/test](/guides/testing/#run-an-app-scoped-harness).

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 on-demand 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:

```ts
it('matches the reference commerce app: wire vocabulary and optimistic statuses', async () => {
  // The on-demand graph artifact of examples/commerce — the
  // rules/v1-acceptance.md acceptance target this tutorial has been building
  // toward without checking in generated output.
  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 = readTempCommerceGraph() as TutorialGraphComparison;
  const commerceCartAdd = commerceGraph.mutations.find((entry) => entry.key === 'cart/add');
  const shopCartAdd = shopGraph.mutations.find((entry) => entry.key === addToCart.key);

  // The tutorial now lets the compiler derive the mutation key from the
  // exported binding and module path; the no-JS form action follows that key.
  const shopPage = await renderShopPageForTest(shopRequest());
  expect(shopPage).toContain(`action="/_m/${addToCart.key}"`);
  expect(shopPage).toContain('name="kovo-form-key" value="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 mutationKeyMap: Record<string, string> = {
    [addToCart.key]: 'cart/add',
  };
  const pairKey = (entry: { mutation: string; query: string }) =>
    `${entry.mutation} ${entry.query}`;
  const shopPairs = shopGraph.optimistic.map((entry) =>
    pairKey({
      mutation: mutationKeyMap[entry.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 normalized (mutation x 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=${cartQuery.key}; product-list=${productsQuery.key}; order-history=${orderHistoryQuery.key}`,
    },
  );
  expect(success.headers['Content-Type']).toBe('text/vnd.kovo.fragment+html; charset=utf-8');
  expect(success.body).toContain(`<kovo-query name="${cartQuery.key}">{"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.key}"},{"domain":"${order.key}"},{"domain":"${product.key}"}]`,
  );

  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"');
}, 120_000);
```

_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](/tutorial/08-wrap-up/#how-this-tutorial-stays-true).
The [testing guide](/guides/testing/) covers pglite-backed harnesses and HTTP-level assertions; the
[kovo explain guide](/guides/kovo-explain/) tours the full command surface.

Guarded, session-typed, and provable without a browser. One short chapter remains: shipping it.

<details>
<summary>Spec & diagnostics</summary>

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`. Complete
explicit access posture: SPEC §10.2, **KV436**. Principal-scoped cart and order-history reads: SPEC
§10.2–§10.3. `observed ⊆ static` write-verification invariant: SPEC §11.2.
Reference-app parity: `rules/v1-acceptance.md`.

</details>
