Skip to main content

Testing

What exists

PackageRunnerWhat it covers
@crag-topo/sharedVitest (Node)Grade normalisation, ID minting, slugs, cursor encoding
@crag-topo/apiVitest in workerd, against a real D1Endpoints, sync pull/push, conflict and rejection paths
@crag-topo/mobileNothing. See gaps
@crag-topo/docsThe buildBroken links fail it
pnpm test # everything
pnpm --filter @crag-topo/api test # just the API
pnpm --filter @crag-topo/shared test:watch

API tests run against a real database

apps/api/vitest.config.ts uses @cloudflare/vitest-pool-workers to run the tests inside workerd with a real D1 instance, migrated from the same migration files production uses.

const migrations = await readD1Migrations(path.join(import.meta.dirname, 'migrations'));

export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: './wrangler.jsonc' },
miniflare: {
bindings: { TEST_MIGRATIONS: migrations, ENVIRONMENT: 'local', GIT_SHA: 'test' },
d1Databases: { DB: 'test-db' },
},
}),
],
test: { setupFiles: ['./test/setup.ts'] },
});

This is not incidental. The bugs worth catching in the sync layer are pagination bugs — a cursor that skips a record when two share a millisecond, a tombstone that never reaches the client. A mocked database returns whatever you told it to and would happily confirm a broken cursor works.

It also means the migrations themselves are exercised on every test run: a migration that no longer parses fails the test suite, not production.

What the API tests actually assert

Worth reading apps/api/test/api.test.ts — but the ones carrying weight:

  • A pull replayed with the returned cursors yields nothing new. This is the whole correctness claim of delta sync in one test.
  • A malformed cursor is a 400. Not a silent fall back to a full table download.
  • A stale baseUpdatedAt produces conflict, with the server's record attached. Not a silent overwrite.
  • A mutation for another user's record is rejected. Not a 500, not applied.
  • Production refuses to serve sync without ALLOW_UNAUTHENTICATED. There is no auth yet (ADR-0007), and this is the test that stops open writes reaching real users by accident.
  • /health reports the git revision. The deploy workflows assert on this, so a change to its shape must break a test.

The gaps

Written down so nobody assumes coverage that is not there.

No mobile tests at all

pnpm --filter @crag-topo/mobile test prints a message and exits zero. That is honest but it is not testing.

The things most worth testing first, in order:

  1. sync.ts — the outbox loop, applying a change set, cursor persistence. This is the most intricate logic in the app and the most expensive to get wrong.
  2. db.ts query helpers — against an in-memory SQLite, checking that soft-deleted rows are filtered and ordering is by sort_index.
  3. topo-overlay.tsx's toPath() — pure geometry, trivially testable, and a wrong curve means route lines that do not match the rock.

expo-sqlite has a Node-compatible path, so 1 and 2 do not need a simulator.

No end-to-end tests

Nothing drives the real app against the real API. Maestro is the usual answer for Expo. The PR preview partly substitutes — a human with a phone is a slow but thorough E2E test — but it is manual.

Sync cases not covered

  • Tombstone propagation: a deleted route reaching a client that was offline
  • Multi-page pulls with hasMore
  • The same-millisecond tie the composite cursor exists to handle — the case most likely to break and the one with no test

No load or limit testing

D1 has row-read and query-duration limits. Nothing measures how close a large crag's /bundle response gets to them.

Conventions

  • Test the seam, not the implementation. The API tests go through app.fetch with real Request objects, so a handler can be rewritten without touching a test.
  • Seed inside the test. beforeEach clears every table (children first, because of the foreign keys) and each test seeds what it needs. No shared fixture state.
  • Assert on failure modes. A test that a 404 has code: 'not_found' is more valuable than a third happy-path test.
  • No snapshot tests of API responses. They fail on every additive change and teach people to re-record without reading.

Adding tests to a package that has none

// package.json
"scripts": { "test": "vitest run" },
"devDependencies": { "vitest": "4.1.10" }

pnpm test at the root runs -r --parallel, so a new package is picked up automatically.