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 })FaceResourceRouterouting, route-precedence, or conflict detectionjsonResourceResponse(),textResourceResponse(),emptyResourceResponse(), ormethodNotAllowedResourceResponse()- docs that show raw JSON/text/empty/method-not-allowed responses
Local expected result:
- resource responses return raw
FaceResponsebodies 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 stable405with a sortedallowheader- 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-sha256for the exact body bytes passed tofetch - unmarked,
GET, anddialogforms 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:
- install the FaceTheory RC tarball exactly in the consuming app;
- mark a same-origin URL-encoded form with
data-facetheory-oac-form; - install
startAwsOacFormTransport()from the client bootstrap module; - route the action path through AppTheory/CloudFront to Lambda, usually with
ssrPathPatterns; - submit through the deployed CloudFront URL and confirm the request reaches Lambda without changing the Function URL
auth type away from
AWS_IAM; - confirm marked multipart/text/plain forms fail closed and do not send a request;
- if the response uses CSP headers, confirm the host handles it through
onResponseoronNavigate.
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(), orvalidateStrictCspDocument()viteHydrationForEntry(),externalHydrationForEntry(), or sidecar URL/data handlingcreateFaceApp({ ssrHydrationSidecars })orcreateSsrHydrationSidecarStore(...)@theory-cloud/facetheory/clienthydration 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>hassrcand no inline body text - no
<style>tags,styleattributes, oron*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
/nextroute 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 Faceload()/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/clientreads 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, orisr
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.