GitBasedDocs has 11 runnable check files and no test framework. How they work, the guard that keeps them off the real database, and why lint, typecheck and build close the gate.
GitBasedDocs has no test runner and no test script in package.json. It still has 11 test files, about 900 lines of them, and all 11 pass in roughly four seconds on my laptop. Each one is a plain TypeScript file that Bun runs directly.
One file beside each unit
The convention is a *.check.ts next to the module it covers. lib/sync/sync.ts has lib/sync/sync.check.ts, lib/github/webhook.ts has lib/github/webhook.check.ts, and so on. A check imports the real functions, calls them, and asserts with Node's built-in node:assert/strict. If an assertion fails, the process throws and exits non-zero. If everything holds, it prints one line:
// Runnable check for the recheck scheduler: bun lib/sync/schedule.check.ts
import assert from "node:assert/strict"
import { intervalFromEnv, nextDelay } from "./schedule"
assert.equal(intervalFromEnv("1"), 5 * MIN, "floor at 5 minutes")
assert.equal(intervalFromEnv("60"), 15 * MIN, "ceiling at 15 minutes")
The first line of each file is the command that runs it. There is nothing to configure. Bun runs TypeScript as is and resolves the @/ path alias from tsconfig.json, so bun lib/viewer/tree.check.ts just works.
What they cover
Seven checks are pure logic and need nothing but Bun:
sync.check.ts: the helpers that decide what a file becomes. Slug from path, title from frontmatter or the first heading, sort order and draft flag. Also the excerpt, and which paths stay hidden.markdown.check.ts: the biggest, at 223 lines. It feeds<script>,onerrorattributes andjavascript:links into the renderer and asserts none of them come out, then covers wikilinks and callouts, math, and code blocks with line marks and diffs.tree.check.ts: sidebar order and folder index pages, plus the breadcrumb trail and prev/next links.webhook.check.ts: signature verification, branch matching, repeat deliveries and both payload shapes GitHub can send.schedule.check.ts: the recheck interval clamp and the rate-limit back-off of 1 minute, then 5, then back to normal.assets.check.tsandcommits.check.ts: MIME types for cached files, and turning GitHub's commit list into who changed a page.
Four checks need a database, because what they test is SQL: access control, search, user management and the danger zone actions. Mocking the database there would test the mock. They run against a real SQLite file with the real migrations applied.
The guard on the database checks
Bun loads .env automatically, and my .env points DATABASE_URL at the app's real database. A database check that ran with that setting would insert test users and projects into it. So each one refuses to start unless the URL names its own scratch file:
if (!process.env.DATABASE_URL?.includes("access-check")) {
console.error("Refusing to run: set DATABASE_URL=file:./data/access-check.db")
process.exit(1)
}
rmSync("./data/access-check.db", { force: true })
const { getDb } = await import("@/lib/db")
The guard runs before the database module is even imported, which is why the imports below it are dynamic. The file is deleted at the start and again at the end, so every run begins from an empty schema and leaves nothing behind. Run it without the variable and you get the refusal, not a polluted database. With it:
DATABASE_URL=file:./data/access-check.db bun lib/access/access.check.ts
Bugs become assertions
The pattern I kept to: when a bug is fixed, the fix commit adds the assertion that would have caught it.
79ad4be: GitHub's webhook form defaults toapplication/x-www-form-urlencoded, and the route only parsed JSON, so a webhook left on that default rejected every push. The fix added checks that both content types parse to the same object, and that the signature covers the raw form body rather than the decoded JSON.6008c45: a folder'sindex.mdrenamed the folder in the sidebar to the page title. The fix added"a titled index must not rename its folder"totree.check.ts.9f8af20: Obsidian keeps deleted notes in.trash, and they were being published.sync.check.tsnow asserts that.trash/old note.mdis hidden and thatnotes/v1.2.mdis not, because a dot inside a file name is fine.
The assertions that pin a bug carry a message saying what broke, so a failure reads as a sentence instead of expected true, got false.
The gate
The final check is three commands, in this order:
bun run lint && bun run typecheck && bun run build
Lint is Next's core-web-vitals and TypeScript config. Typecheck is tsc --noEmit, and since tsconfig.json includes every .ts file, the checks get type-checked with everything else. The build is last because it is the slowest and the one that proves the app actually compiles as a whole.
Why this was enough
The risk in this app sits in a handful of pure functions and a few queries: who can see a project, what a file turns into, whether a webhook is genuine, and when to back off from GitHub. Those are exactly what the checks pin down. The React components are thin, and they are checked by opening the page.
A runner would add a dependency and a config file, plus a second way of loading TypeScript. What I would lose without one is watch mode and a summary table, and for 11 files that run in four seconds I have not missed either.