QUESTPIE
CodeTesting

Actors

Four factories on the harness, the user seed each one wants, and what the callback gets. An actor is how a test says who is calling.

View markdown

Which factory do you reach for, and what does it hand your callback? Every actor comes off the createTestApp result and shares one method, run().

The factories

Factorykindcontext.sessioncontext.accessMode
anonymous()"anonymous"null"user"
actor({ user })"user"A built session"user"
actor({ session })"user"Your session"user"
oauth({ session, clientId, scopes, tokenId })"oauth"Your session"user"
system()"system"undefined"system"

An actor is cheap and stateless. Build one per identity at the top of the file and reuse it. Each run() call builds a fresh context.

const editor = testApp.actor({ user: editorSeed });
const viewer = testApp.actor({ user: viewerSeed });

const post = await editor.run(({ app, context }) =>
	app.collections.posts.create({ title: "Draft", slug: "draft" }, context),
);

run() returns whatever the callback returns, awaited. It also opens the AsyncLocalStorage scope, so getContext() resolves inside the callback and a nested call with no context of its own inherits this one.

The user seed

actor({ user }) takes your app's user row, not a partial. Every field your user table requires is required here too, including the ones your auth config added. Three keys are optional, and the harness fills them in.

KeyDefault
emailVerifiedtrue
createdAt2020-01-01T00:00:00.000Z
updatedAtThe same timestamp

It then wraps that user in a session. The id is <userId>-session, the token is <userId>-token, and it expires on 2100-01-01. Nothing is written to the database. The session exists to carry identity into your access rules.

const editor = testApp.actor({
	user: {
		id: "editor-1",
		email: "editor@example.test",
		name: "Editor",
		role: "editor",
		tenantId: "tenant-1",
	},
});

Some apps must pass the whole session

A Better Auth plugin can add a required field to the session record, such as activeOrganizationId. The harness only knows the standard keys, so {user} stops compiling for that app. Pass {session} and set the field yourself.

{ session } is always allowed. Reach for it when the seed will not do, or when you want one session shared by a user actor and an OAuth actor.

OAuth actors

oauth() needs a full session plus the three values a token carries.

const session = await editor.run(({ context }) => context.session);

const agent = testApp.oauth({
	session,
	clientId: "test-client",
	scopes: ["collections:posts:read"],
	tokenId: "token-1",
});

That builds a principal of { kind: "oauth", user, clientId, scopes, tokenId } and puts it on the context. The access mode stays "user", so your own rules still run as that user. The scopes are the extra gate on top. A rule that reads principal.scopes sees this list, and so does the MCP scope gate. See Scopes.

System actors bypass the rules

system() sets accessMode: "system" and a { kind: "system" } principal. Use it to arrange state, then switch to a user actor for the assertion.

await testApp
	.system()
	.run(({ app, context }) =>
		app.collections.posts.create({ title: "Seeded", slug: "seeded" }, context),
	);

const visible = await viewer.run(({ app, context }) =>
	app.collections.posts.findOne({ where: { slug: "seeded" } }, context),
);

Field-level access shows the split. Give a field access: { read: false }. The system actor reads its value. The user actor gets a row where that key is absent, not null.

Typing a hand-built app

Codegen writes the session type onto createAppForRuntime, so the actors on a generated app type themselves. An app you assemble yourself, such as one inside a module's own test suite, has no such marker. Annotate the factory and the same inference follows.

import { createApp } from "questpie/app";
import type { GeneratedAppFactory } from "@questpie/testing";

const createAppForRuntime: GeneratedAppFactory<MyApp, MySession> = async (
	runtime,
) => (await createApp(definition, runtime)) as unknown as MyApp;

MySession is the { user, session } pair your auth config produces. It drives the seed shape, the { session } input and context.session in every callback.

  • Testing for the harness that hands out these actors.
  • Context for every other key on context.
  • Access control for the rules an actor exercises.

On this page