This doc explains how Orin is tested today, why we bother, and what you get out of it. No prior testing experience required.
Orin has 66 unit tests in 14 files, all under lib/. They run in about a second with:
npm testWe test pure logic on the server — quotas, encryption, redirects, message cleanup — not the full UI or live APIs.
That matches Phase 5 of the architecture plan: lock in the code where bugs would cost money, leak secrets, or break trust, without building a huge test suite for a portfolio project.
Most of Orin is a web app: buttons, chat bubbles, voice overlays. You could click through everything manually before every change. That works once. It does not scale.
Tests are automated checks that run the same way every time. They answer: “Did I break something important?” before you merge or deploy.
For Orin specifically, the scary failures are not “button is 2px off.” They are:
| If this breaks… | What goes wrong |
|---|---|
| Quota checks | Someone uses your OpenAI/ElevenLabs keys for free, or paying users get blocked wrongly |
| API key encryption | User BYOK keys stored in plaintext or shown in the UI |
| Auth redirects | Login sends people to a malicious site (?next=https://evil.com) |
| Message sanitization | Empty or broken messages confuse the model or waste tokens |
Those paths live in lib/ — plain TypeScript with inputs and outputs. That is the easiest and highest-value place to test.
Each row is a real file (or group) in the repo.
| Area | Test file(s) | What it proves |
|---|---|---|
| Quota limits | lib/quotas/limits.test.ts |
Anonymous vs signed-in caps, voice minute billing, admin bypass |
| Quota + BYOK resolution | lib/quotas/resolve.test.ts, keys.test.ts |
Over limit → signup or “add keys”; under limit → platform key |
| Quota API errors | lib/quotas/errors.test.ts, client-errors.test.ts |
Stable error codes for the UI (signup_required, keys_required) |
| BYOK crypto | lib/crypto/secrets.test.ts |
Encrypt/decrypt round-trip; masked keys never show the full secret |
| Auth redirects | lib/auth/safe-redirect.test.ts, return-url.test.ts, post-auth.test.ts |
Only same-origin paths allowed after login; anon session merge |
| Rate limiting | lib/security/rate-limit.test.ts |
API rate limits block abuse |
| Chat input hygiene | lib/ai/message-utils.test.ts |
Empty turns stripped before hitting the model |
| Personality config | lib/orin/personality/parse.test.ts, prompts.test.ts |
Bad cookie/DB input falls back safely; prompts assemble |
| Voice disconnect UX | lib/voice/disconnect-toast.test.ts |
ElevenLabs disconnect reasons map to the right toast |
Total: 66 tests across those modules.
We are not trying to test everything. These are out of scope on purpose:
- Full browser flows (open app → sign up → chat → voice call)
- React / Nexus UI components (snapshots, “does this button render”)
- Live Supabase, OpenAI, or ElevenLabs in CI
- The voice sidecar WebSocket server end-to-end
Those need real services, browsers, and time. They are valuable later; they are not required for the current strategy.
Manual testing and preview deploys still cover “does the app feel right?” Unit tests cover “did we break the rules of the system?”
A unit test calls one function (or a small group) with a fixed input and checks the output.
Example idea (not exact code):
expect(safeRedirectUrl("//evil.com")).toBe("/new");If someone changes safeRedirectUrl and open redirects work again, this test fails immediately with a clear message. You fix the code before users get phished.
No browser. No database. Usually no network. Fast.
Tests live next to the code they protect:
lib/quotas/limits.ts
lib/quotas/limits.test.ts
When you change limits.ts, the matching .test.ts is right there. You are more likely to update both.
Some modules normally talk to Supabase (createAdminClient). In tests we mock that: we replace the database with a fake that returns canned numbers.
That lets us test “when usage is 8/8, block the anon user” without a real Postgres project.
Quota resolution tests mock isUnderQuota and getStoredUserKeys so we can simulate “over limit + no BYOK” without seeding rows.
Many lib/ files import "server-only" (Next.js guard: this must not run in the browser). Vitest stubs that import via vitest.server-only-stub.ts.
Crypto tests need API_KEY_ENCRYPTION_SECRET. Vitest sets a dummy value in vitest.setup.ts so you do not need .env.local to run tests.
# Run once (CI does this)
npm test
# Re-run on file save while you work
npm run test:watchYou should see 14 passed test files, 66 passed tests. No API keys or Supabase project required.
On every pull request and push to main, the CI workflow runs npm test and npm run build. If a test fails, the check goes red and the PR should not merge until it is fixed.
That stops quota or crypto regressions from slipping in when you (or a contributor) change lib/ without running tests locally.
Add or extend a test when you change logic that:
- Enforces limits or billing-like rules (
lib/quotas/) - Handles secrets (
lib/crypto/) - Decides where users go after auth (
lib/auth/safe-redirect.ts,lib/auth/return-url.ts,lib/auth/post-auth.ts) - Shapes what the model sees (
lib/ai/message-utils.ts, personality parse/build)
Skip tests for:
- One-off UI layout tweaks
- Copy / styling
- Thin wrappers that only call an API with no branching
Rule of thumb: if a bug would embarrass you in a demo or cost real API money, it deserves a unit test in lib/.
- Create
lib/your-module.test.tsnext tolib/your-module.ts. - Import
describe,it,expectfromvitest. - Run
npm test.
Vitest picks up any lib/**/*.test.ts automatically (see vitest.config.ts).
Example skeleton:
import { describe, expect, it } from "vitest";
import { myFunction } from "@/lib/my-module";
describe("myFunction", () => {
it("does the important thing", () => {
expect(myFunction("input")).toBe("expected");
});
});By the time you merge:
- Quota bypass in shared resolution logic is caught before deploy.
- BYOK crypto cannot silently regress to leaking full keys in masks.
- Open redirects on login stay blocked.
- Garbage messages do not get sent to the model.
- Contributors (open source) get a fast, zero-setup command to verify
lib/behavior.
It does not prove the whole product works in a browser. It proves the rules Orin is built on still hold. For a project of this size, that is the right tradeoff.