Testing your app
Run Vitest on an app built on the framework — the server-only stub, why the framework packages are inlined, keeping models out of CI, and what to test.
At the end of this page pnpm test runs your app’s own tests: the seams you bind, your tools and your server actions, with no model calls and no browser.
Add the setup
intelligo add vitest
pnpm add -D vitestThen add "test": "vitest run" to the scripts in package.json. intelligo add vitest writes two files and records them in intelligo.manifest.json like every other generated file, so upgrade --check can tell you when the template moves.
import path from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
const root = path.dirname(fileURLToPath(import.meta.url));
/**
* The app's test runner.
*
* - `@` resolves the way the app's tsconfig resolves it.
* - `server-only` throws unless the bundler applies React's
* `react-server` condition, which a test does not; the stub stands in
* for it so a test can import server code.
* - The `@intelligo-dev/*` packages ship compiled ESM that imports
* `server-only` itself. Vitest leaves node_modules to Node, where the
* alias above never applies, so they are inlined: transformed by
* Vitest like the app's own source, with the alias in effect.
*/
export default defineConfig({
resolve: {
alias: {
"@": root,
"server-only": path.join(root, "tests/stubs/server-only.ts"),
},
},
test: {
environment: "node",
include: ["**/*.test.{ts,tsx}"],
exclude: ["node_modules/**", ".next/**"],
server: {
deps: {
inline: [/node_modules\/@intelligo-dev\//],
},
},
},
});The second file, tests/stubs/server-only.ts, is an empty module.
Why the framework packages are inlined
The framework’s server modules begin with import "server-only", the guard that fails a build which pulls server code into the browser. Outside a react-server build the real package throws on import. An alias to the stub fixes that for your own files — but Vitest hands anything under node_modules to Node untransformed, and Node never sees a Vite alias. The first test that imports lib/intelligo.ts would then fail inside @intelligo-dev/billing on the guard, not on anything it tests.
server.deps.inline makes Vitest transform the @intelligo-dev/* packages itself, so the alias reaches their imports too. Mock a package by its specifier (vi.mock("@intelligo-dev/core/documents")), never by a path inside node_modules: the packages ship compiled ESM, and the file layout is not part of their contract.
No model calls in CI
Mock the AI SDK provider you use, so a test never needs a key and never spends one:
import { vi } from "vitest";
vi.mock("@ai-sdk/anthropic", () => ({
anthropic: vi.fn(() => ({ modelId: "anthropic/claude-sonnet-4-6" })),
}));For a whole chat turn, @intelligo-dev/chat/testing exports createStubLanguageModel: a language model that answers from a function of the last user message, can emit a tool call, and reports token usage like a real one. The scaffold’s chat streams from it until you configure a provider.
Environment read at import time
A module that reads the environment when it loads — a config object, a provider client built at the top of a file — sees whatever was set before the import. Stub the variables first and import afterwards, with a fresh module registry per test:
import { beforeEach, expect, it, vi } from "vitest";
beforeEach(() => {
vi.resetModules();
vi.stubEnv("NEXT_PUBLIC_APP_URL", "https://app.example.com");
});
it("builds links from the public URL", async () => {
const { appUrl } = await import("@/lib/app-url");
expect(appUrl("/login")).toBe("https://app.example.com/login");
});What is worth testing
Most of an app built on the framework is installed source the registry already tests, and intelligo sync --check keeps it the registry’s. What only your tests reach is what you decided:
- The seams you bind —
lib/chat-server-config.tsruns on a registered model id and gates on a feature key your plans declare;lib/chat-config.tsxnames starters your messages define. The reference app’slib/__tests__has one of each. - Your tools and prompts — the functions an agent calls, with the model mocked.
- Your Server Actions — parse, call the service, map its typed error. Mock the service’s package and assert the mapping.
- Your own tables — against a real Postgres, skipped when there is none:
describe.skipIf(!process.env.TEST_DATABASE_URL).