WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

Test Coverage Analysis#

Current State: No Tests Exist#

The monorepo has zero test infrastructure. No testing framework is installed, no test scripts exist in any package.json, and turbo.json has no test task. There are approximately 530 lines of source code across 4 packages with 0% test coverage.


Vitest is the best fit for this project:

  • Native ESM support (the repo uses "type": "module" everywhere)
  • Built-in TypeScript support via tsx/esbuild (no separate ts-jest config)
  • Workspace-aware — can share a root config while per-package configs override as needed
  • Fast, with watch mode out of the box

Infrastructure needed#

  1. Install vitest as a root devDependency
  2. Add a root vitest.workspace.ts pointing at each package
  3. Add "test": "vitest run" scripts to each package's package.json
  4. Add a "test" task to turbo.json (with dependsOn: ["^build"] since appview/web depend on lexicon types)
  5. Add pnpm test as a root script

Package-by-Package Gaps and Recommendations#

1. @atbb/appview — API Server (highest priority)#

This is the most complex package (260 LOC) and where most business logic will land as stubs are implemented. It has the database schema, config loading, AT Protocol agent creation, and all API routes.

a) Route handler tests (high value)#

Files: src/routes/health.ts, src/routes/forum.ts, src/routes/categories.ts, src/routes/topics.ts, src/routes/posts.ts

What to test:

  • GET /api/healthz returns 200 with { status: "ok", version: "0.1.0" }
  • GET /api/healthz/ready returns 200 with { status: "ready" }
  • GET /api/forum returns 200 with the expected forum shape
  • GET /api/categories returns 200 with { categories: [] }
  • GET /api/categories/:id/topics returns 200 and echoes the id param
  • POST /api/topics returns 501 (not implemented)
  • POST /api/posts returns 501 (not implemented)

How: Use Hono's built-in app.request() test helper — no HTTP server needed:

import { describe, it, expect } from "vitest";
import { apiRoutes } from "../src/routes/index.js";
import { Hono } from "hono";

const app = new Hono().route("/api", apiRoutes);

describe("GET /api/healthz", () => {
  it("returns ok status", async () => {
    const res = await app.request("/api/healthz");
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual({ status: "ok", version: "0.1.0" });
  });
});

Why it matters: As stubs are replaced with real implementations, having route-level tests in place catches regressions in response shape, status codes, and content-type headers. These tests are cheap to write now and will grow in value.

b) Config loading tests (medium value)#

File: src/lib/config.ts

What to test:

  • Returns correct defaults when env vars are absent (PORT defaults to 3000, PDS_URL defaults to https://bsky.social)
  • Parses PORT as an integer (not a string)
  • Returns provided env var values when set
  • Handles PORT set to a non-numeric string (currently parseInt would return NaN — should this throw?)

Why it matters: Config loading is the root of most "it works on my machine" bugs. The current implementation silently accepts empty strings for forumDid and databaseUrl, which will cause hard-to-debug runtime failures. Tests would document this behavior and motivate adding validation.

c) Database schema tests (medium value)#

File: src/db/schema.ts

What to test:

  • Schema definitions export the expected table names
  • Column types match expectations (e.g., posts.deleted defaults to false)
  • Foreign key references are correct (posts.didusers.did, posts.rootPostIdposts.id, etc.)
  • Index names are correct and unique constraints are in place

How: These can be pure unit tests against the Drizzle schema objects — no database connection needed. Drizzle table objects expose ._.columns and other metadata you can assert against.

Why it matters: Schema is the foundation. If someone accidentally removes an index or changes a foreign key, these tests catch it before it hits a migration.

d) Database integration tests (high value, but requires infrastructure)#

What to test:

  • Insert/select/update/delete for each table
  • Foreign key constraints are enforced (e.g., inserting a post with a non-existent did fails)
  • Unique index violations behave as expected
  • The createDb() factory produces a working Drizzle client

How: Use a test PostgreSQL instance. Options:

  • Testcontainers (Docker-based, spins up a real Postgres per test suite)
  • pg-mem (in-memory Postgres emulator, faster but not 100% compatible)
  • A shared test database with transaction rollback between tests

Why it matters: This is where the most subtle bugs live — constraint violations, bad joins, missing indexes. As the appview stubs are fleshed out with real queries, these tests become critical.

e) AT Protocol agent factory test (low value now, higher later)#

File: src/lib/atproto.ts

Currently just new AtpAgent({ service: config.pdsUrl }) — not much to test. But as authentication and record-writing logic is added, this module should have tests verifying:

  • Agent is created with the correct service URL
  • Authentication errors are handled gracefully
  • Record write/read operations produce expected AT URI formats

2. @atbb/web — Server-Rendered Web UI#

a) API client tests (high value)#

File: src/lib/api.ts

What to test:

  • fetchApi("/categories") calls the correct URL (${appviewUrl}/api/categories)
  • Throws an Error with status code and status text on non-2xx responses
  • Returns parsed JSON on success
  • Handles network failures (fetch throws)

How: Mock global.fetch with vi.fn() or use msw (Mock Service Worker):

import { describe, it, expect, vi, beforeEach } from "vitest";

// Mock fetch globally
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

describe("fetchApi", () => {
  it("throws on non-ok response", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false, status: 500, statusText: "Internal Server Error",
    });
    const { fetchApi } = await import("../src/lib/api.js");
    await expect(fetchApi("/test")).rejects.toThrow("AppView API error: 500");
  });
});

Why it matters: fetchApi is the single point of contact between the web UI and the appview. Error handling here determines whether users see useful error messages or blank pages.

b) JSX component / layout tests (medium value)#

File: src/layouts/base.tsx, src/routes/home.tsx

What to test:

  • BaseLayout renders valid HTML with the provided title
  • BaseLayout uses the default title "atBB Forum" when none is provided
  • BaseLayout includes the HTMX script tag
  • Home route returns 200 with text/html content type
  • Home page includes "Welcome to atBB" heading

How: Use Hono's app.request() and assert against the HTML string, or use a lightweight HTML parser. Hono JSX components can be tested by rendering them and checking the output string.

c) Config loading tests (low-medium value)#

File: src/lib/config.ts

Same pattern as the appview config tests — verify defaults, parsing, and presence of required values.


3. @atbb/lexicon — Lexicon Definitions#

a) YAML-to-JSON build script tests (medium value)#

File: scripts/build.ts

What to test:

  • Each YAML file in lexicons/ produces valid JSON
  • Output JSON matches the expected Lexicon schema structure (has lexicon, id, defs fields)
  • No duplicate lexicon IDs across files
  • The id field in each lexicon matches its file path (e.g., space/atbb/post.yaml has id: "space.atbb.post")

How: Rather than testing the build script directly (it's I/O-heavy), write validation tests that run against the YAML source files:

import { parse } from "yaml";
import { readFileSync } from "fs";
import { glob } from "glob";

describe("lexicon definitions", () => {
  const files = glob.sync("**/*.yaml", { cwd: "lexicons" });

  it.each(files)("%s has a valid lexicon structure", (file) => {
    const content = readFileSync(`lexicons/${file}`, "utf-8");
    const parsed = parse(content);
    expect(parsed).toHaveProperty("lexicon", 1);
    expect(parsed).toHaveProperty("id");
    expect(parsed).toHaveProperty("defs");
  });
});

Why it matters: Lexicon definitions are the API contract for the entire AT Protocol integration. A malformed lexicon causes downstream build failures in type generation and runtime validation errors. Catching issues at the YAML level is far cheaper than debugging them at the API level.

b) Schema contract tests (high value)#

What to test:

  • space.atbb.post has text as a required string field
  • space.atbb.post has optional reply with root and parent refs
  • space.atbb.forum.forum uses key: literal:self
  • space.atbb.forum.category uses key: tid
  • All strongRef usages have both uri and cid fields
  • knownValues are used (not enum) for extensible fields like modAction.action

Why it matters: These are the contract tests of the system. If a lexicon field is accidentally renamed or a required field becomes optional, it breaks interoperability with any PDS that stores atBB records. These tests protect the public API surface.


4. @atbb/spike — PDS Integration Script#

The spike is a manual integration test. It doesn't need unit tests itself, but:

Extractable test utilities (medium value)#

The spike contains reusable patterns for:

  • Authenticating with a PDS
  • Creating/reading/deleting AT Protocol records
  • Generating TIDs

These should be extracted into a shared test utility module (e.g., packages/test-utils/) that integration tests across the monorepo can use.


Priority Matrix#

Priority Area Package Effort Impact
P0 Test infrastructure setup (vitest, turbo task, CI) root Low Unblocks everything
P0 Appview route handler tests appview Low Catches regressions as stubs are implemented
P1 Web API client tests (fetchApi) web Low Validates the only web→appview boundary
P1 Lexicon schema contract tests lexicon Low Protects the AT Protocol API surface
P1 Config loading tests (both packages) appview, web Low Documents defaults, catches parse bugs
P2 Database schema unit tests appview Medium Catches accidental schema changes
P2 JSX layout/component tests web Medium Ensures correct HTML output
P2 Lexicon build script validation lexicon Low Catches YAML/JSON conversion issues
P3 Database integration tests appview High Requires Postgres test infra (Docker/testcontainers)
P3 AT Protocol integration tests appview High Requires PDS test instance or mock
P3 Extract spike utilities into shared test-utils spike Medium Enables reuse across integration tests

Suggested Implementation Order#

  1. Set up vitest at the root + per-package, add test task to turbo.json
  2. Appview route tests — quick wins since Hono has a built-in test helper and the routes are simple right now
  3. Lexicon contract tests — validate YAML schema structure to protect the AT Protocol API
  4. Web fetchApi tests — mock fetch, verify URL construction and error handling
  5. Config tests for both packages — small but catches real bugs
  6. Database schema tests — assert on Drizzle metadata objects
  7. Database integration tests — add testcontainers or similar once there are real queries to test