Guides

FaceTheory Testing Guide

This guide covers the verification commands that back the public contract and the evidence expected before a push or release.

Test Strategy

FaceTheory verification is centered on deterministic runtime behavior in the TypeScript workspace.

Primary goals:

  • Validate request normalization, routing, buffered SSR, streaming SSR, SSG, ISR, and framework adapter behavior.
  • Keep example commands runnable so docs and implementation do not drift apart.
  • Capture enough evidence to distinguish toolchain issues from runtime regressions.

Baseline Verification

Run the standard checks:

cd ts
npm run typecheck
npm test

Equivalent root wrappers after dependencies are installed:

make ts-typecheck
make ts-test

Expected result:

  • Type checking completes with no errors.
  • The unit suite passes.

Test Your Faces With @theory-cloud/facetheory/testing

Consumer apps can unit-test a Face without constructing Lambda Function URL events or copying repository-only harnesses. The testing subpath is Node test tooling and is intentionally separate from the browser client helper surface:

import {
  assertHydrationEquivalent,
  buildFaceRequest,
  renderFace,
} from "@theory-cloud/facetheory/testing";

const request = buildFaceRequest({
  url: "https://checkout.example.test/checkout?cart=cart_test",
  headers: { cookie: "session=test-session" },
  cspNonce: "nonce-test",
});

const rendered = await renderFace(checkoutFace, { request });

assert.equal(rendered.status, 200);
assert.match(rendered.html, /Checkout/);

await assertHydrationEquivalent({
  html: rendered.html,
  selector: "#root",
  hydrate: async ({ document }) => {
    // Wire the same framework hydrate call your client bootstrap uses.
    // React example: hydrateRoot(document.getElementById("root")!, <App />)
  },
});

buildFaceRequest() returns a deterministic FaceRequest with a stable x-request-id and parses URL query strings for you. renderFace() accepts a FaceModule, an array of Faces, or an app-like object with handle(request) and returns the collected response body as html/text so tests can assert against complete documents.

assertHydrationEquivalent() uses a jsdom-backed browser harness, installs browser globals for the duration of the assertion, captures console.error/console.warn, fails on framework hydration-mismatch messages, and compares the selected DOM subtree before and after the consumer-provided hydrate callback. It fails closed on unexpected fetch() by default; pass a fixture fetcher when a strict-CSP route intentionally loads external hydration data:

import {
  assertHydrationEquivalent,
  createStrictCspFixtureFetch,
} from "@theory-cloud/facetheory/testing";

const { fetcher } = createStrictCspFixtureFetch({
  "/_facetheory/data/page.json": { cartId: "cart_test" },
});

await assertHydrationEquivalent({
  html,
  selector: "#root",
  fetcher,
  hydrate: async (ctx) => {
    // Load external hydration data, then call the framework hydrate primitive.
  },
});

Strict-CSP test helpers are also exported for application suites that need the same no-inline assertions FaceTheory uses internally:

import { assertStrictCspDocument } from "@theory-cloud/facetheory/testing";

await assertStrictCspDocument(rendered.html);

The DOM helpers dynamically load jsdom. Keep jsdom in the consuming test workspace’s dev dependencies; it is not part of FaceTheory’s browser/runtime contract.

Focused Verification Paths

Run these targeted flows when a change touches one delivery mode or adapter more than the rest of the runtime.

SSG

cd ts
npm run example:ssg:build
npm run example:ssg:serve

Use this when changing:

  • route planning
  • hydration data output
  • static file layout

Resource Routes

cd ts
node --import tsx test/unit/resource.test.ts
node --import tsx test/unit/app.test.ts

Use this when changing:

  • createFaceApp({ resources })
  • FaceResourceRoute routing, route-precedence, or conflict detection
  • jsonResourceResponse(), textResourceResponse(), emptyResourceResponse(), or methodNotAllowedResourceResponse()
  • docs that show raw JSON/text/empty/method-not-allowed responses

Local expected result:

  • resource responses return raw FaceResponse bodies rather than HTML documents
  • helper-owned headers are lower-case, sorted, deterministic, and default to cache-control: no-store
  • JSON helpers apply the same HTML-significant escaping used by FaceTheory document serialization
  • methodNotAllowedResourceResponse() emits a stable 405 with a sorted allow header
  • exact duplicate and same-precedence ambiguous Face/resource routes fail closed during app construction

OAC Mutating Form Transport

cd ts
npx tsx test/unit/oac-form.test.ts

Use this when changing:

  • startAwsOacFormTransport()
  • URL-encoded form field collection or payload hashing
  • AppTheorySsrSite OAC mutating-form documentation
  • response navigation, CSP, redirect, or unsupported-encoding behavior for marked forms

Local expected result:

  • marked same-origin POSTs send content-type: application/x-www-form-urlencoded;charset=UTF-8
  • marked same-origin POSTs send x-amz-content-sha256 for the exact body bytes passed to fetch
  • unmarked, GET, and dialog forms keep native behavior
  • cross-origin actions and marked unsupported encodings fail before sending
  • mutating fetches use redirect: "error"
  • default HTML document replacement refuses CSP-protected responses unless the host handles the response

Release-candidate validation for an AppTheorySsrSite consumer should use the published GitHub Release tarball, not a workspace link:

  1. install the FaceTheory RC tarball exactly in the consuming app;
  2. mark a same-origin URL-encoded form with data-facetheory-oac-form;
  3. install startAwsOacFormTransport() from the client bootstrap module;
  4. route the action path through AppTheory/CloudFront to Lambda, usually with ssrPathPatterns;
  5. submit through the deployed CloudFront URL and confirm the request reaches Lambda without changing the Function URL auth type away from AWS_IAM;
  6. confirm marked multipart/text/plain forms fail closed and do not send a request;
  7. if the response uses CSP headers, confirm the host handles it through onResponse or onNavigate.

For the original lab driver, theory-mcp-server should validate POST /agents/new through CloudFront OAC before stable promotion. A successful RC validation means the app-local workaround can be removed while keeping OAC enabled.

Strict CSP Hydration And Navigation

cd ts
npm run example:vite:svelte:strict-csp:build
node --import tsx test/unit/strict-csp-harness.test.ts
node --import tsx test/unit/vite-strict-csp-svelte-example.test.ts

Use this when changing:

  • FaceCspPolicy, buildStrictCspHeader(), or validateStrictCspDocument()
  • viteHydrationForEntry(), externalHydrationForEntry(), or sidecar URL/data handling
  • createFaceApp({ ssrHydrationSidecars }) or createSsrHydrationSidecarStore(...)
  • @theory-cloud/facetheory/client hydration loading
  • adapter strict-CSP enforcement for React, Vue, or Svelte
  • startFaceNavigation() external hydration loading or same-origin validation
  • docs that describe strict no-inline CSP, external hydration, or Svelte/Vite strict examples

Local expected result:

  • rendered documents contain no __FACETHEORY_DATA__ inline JSON script
  • every <script> has src and no inline body text
  • no <style> tags, style attributes, or on* event-handler attributes appear in validated scopes
  • strict Svelte/Vite output uses external CSS/assets and a same-origin module bootstrap
  • the browser harness loads external hydration JSON before initial hydration and before hydrateFaceNavigation(context)
  • same-origin navigation to the strict example’s /next route preserves deterministic server/client hydration data
  • framework-owned SSR sidecars emit /_facetheory/ssr-data/..., return raw no-store JSON from the same FaceApp handler, and do not increment Face load()/render() counts when fetched
  • caller-managed externalHydrationForEntry(...) URLs are preserved and do not trigger framework sidecar writes
  • SSG strict hydration sidecars use /_facetheory/data/* build artifacts, while ISR strict hydration sidecars stay paired with the cached HTML through the ISR runtime instead of using the SSR sidecar prefix
  • loadFaceHydrationData() from @theory-cloud/facetheory/client reads inline hydration first, fetches same-origin external hydration when linked, and rejects unsafe schemes, cross-origin URLs, cross-origin redirects, and non-JSON sidecar responses

For documentation reviews, explicitly check the unsafe-claim boundary: these tests prove local runtime behavior and example wiring, not that a release has been published, a Simulacrum RC has been validated, or an AWS/customer deployment has succeeded.

Svelte SSR Fixture Harness

cd ts
node --import tsx test/unit/svelte-ssr-fixtures.test.ts

Use this when changing:

  • Svelte Stitch or responsive primitive components
  • createSvelteFace() or Svelte adapter SSR behavior
  • fixture definitions or stored snapshots under ts/test/fixtures/svelte-ssr/
  • the Svelte compiler floor or lockfile version

Local expected result:

  • every Svelte component in the fixture roots has exactly one definition and one stored snapshot
  • SSR, SSG, and ISR render the same body for each fixture
  • repeated SSR renders are byte-identical for the current fixture baseline

These snapshots are tied to the current svelte/compiler output shape pinned by ts/package-lock.json. They are a baseline for FaceTheory’s adapter/mode determinism and for reviewing the Svelte 5 runes migration; they are not a portable proof that legacy-Svelte and runes-compiled components emit byte-identical HTML forever. When snapshots are regenerated with FACETHEORY_UPDATE_SVELTE_SSR_FIXTURES=1, review compiler-owned comment/anchor marker drift separately from visible HTML drift and record intentional output changes in the migration or release notes.

The harness uses a direct svelte/compiler path with a temporary .mjs module tree so wrapper fixtures can exercise default and named snippets through FaceTheory’s adapter without depending on Vite/Rollup bundling. That direct path has narrow import rewrites for the current compiler output; if those assertions fail, update the harness before accepting a snapshot change. Production bundler behavior remains covered by the Vite Svelte example builds. Scratch directories use the ignored .tmp-facetheory-svelte-ssr-fixtures-* prefix so an interrupted run cannot accidentally stage generated modules.

React SSR And Streaming

cd ts
npm run example:buffered:serve
npm run example:streaming:serve

Use this when changing:

  • head rendering
  • streaming behavior
  • style extraction timing

Operator Visibility SSR Example

cd ts
npm run example:operator-visibility:build
npx tsx test/unit/operator-visibility-example.test.ts

Use this when changing:

  • Stitch admin operator visibility primitives
  • deterministic guard/authority/confidence/staleness/correlation rendering
  • health panels or visibility matrices used by operator dashboards

The example intentionally passes stable age labels, normalized correlation IDs, guard status, health observations, and matrix cells through Face load() data. Do not compute freshness or correlation from Date.now(), browser globals, auth/session state, or network calls during render.

For operator dashboard documentation or integration reviews, also confirm:

  • AppTheory/Autheory-derived auth state is passed into FaceTheory as OperatorGuardStatus; FaceTheory docs and examples do not embed Autheory validation or product-specific authorization logic.
  • Live auth-varying dashboards use SSR or a deterministic SPA shell. SSG is limited to static snapshots, and ISR examples call out explicit cache/tenant partitioning for every request-varying dimension.
  • Empty, loading, unauthorized, and filtered states do not use production-like mock partner, tenant, release, account, or version values.

Vite SSR Adapters

cd ts
npm run example:vite:ssr:build && npm run example:vite:ssr:serve
npm run example:vite:vue:build && npm run example:vite:vue:serve
npm run example:vite:svelte:build && npm run example:vite:svelte:serve

Use this when changing:

  • manifest asset injection
  • framework adapter parity
  • hydration bootstrap behavior

High-Signal Test Areas

These areas provide the fastest signal that a change has altered public behavior rather than only internal implementation details.

Representative unit coverage includes:

  • HTTP and app runtime behavior
  • Lambda Function URL conversion
  • React streaming and style handling
  • SSG planning and output layout
  • ISR regeneration and cache state handling
  • Vue, Svelte, and Vite example coverage
  • AWS S3 and TableTheory adapter behavior

Evidence To Capture

Capture enough context that another engineer can reproduce a failure without reverse-engineering your environment from scratch.

For every regression or risky change, capture:

  • command run
  • pass or fail result
  • failing test names or stack traces
  • Node.js version in use
  • the adapter or mode involved, such as react, vue, svelte, ssg, or isr

For example-driven verification, also capture:

  • URL checked
  • expected versus actual headers
  • generated output path if the flow writes files

Operator Verification

Production and staging checks belong with the AWS operator docs so they stay aligned with the deployed topology.

Deployed checks for CloudFront, S3, Lambda URL, or ISR state belong in CDK And AWS Notes.