npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hazo_testing

v0.3.1

Published

Shared test infrastructure for hazo apps: SQLite test DB, real-package wiring, auth factories, apiTestClient, fixtures, Jest + Vitest presets.

Readme

hazo_testing

Shared test infrastructure for hazo apps. Gives a consumer's Jest suite:

  1. A real SQLite test database (in-memory sql.js via hazo_connect) with ordered migration application and teardown.
  2. Auth factories that write real hazo_auth user/scope rows and mint session cookies.
  3. Real-package wiring helpers that stand up hazo_jobs, hazo_notify, and hazo_llm_api against the test DB — no fakes.
  4. An apiTestClient, seed fixtures, a Jest preset, and in-process reset hooks.
  5. Optional sub-exports for Playwright and Next.js integration testing.

WARNING: real external I/O.

  • wireHazoNotify with channels: ["email"] or ["telegram"] will send real email and real Telegram messages through ZeptoMail / Telegram Bot API. Tests that enable those channels are skip-gated on the presence of ZEPTOMAIL_API_KEY / telegram credentials.
  • wireHazoLlmApi makes real, billed API calls to Gemini / Qwen. Tests that call llm.run(...) are skip-gated on GEMINI_API_KEY / QWEN_API_KEY.
  • The inbox-only notify path and all jobs/fixture/factory tests need no credentials and run fully offline.

Install

npm install --save-dev hazo_testing

Peer dependencies (install all that your test suite exercises):

npm install hazo_core hazo_connect hazo_auth hazo_jobs hazo_notify hazo_llm_api

Quick start

// your_suite.test.ts
import { createTestDatabase, createTestUser, seedTestData } from "hazo_testing";

let db: Awaited<ReturnType<typeof createTestDatabase>>;

beforeAll(async () => {
  process.env.JWT_SECRET = "test-secret-min-32-chars-12345678";
  db = await createTestDatabase({
    setupSqlFiles: [
      "node_modules/hazo_auth/db_setup_sqlite.sql",
    ],
  });
  await seedTestData(db.adapter, { fixture: "small_dataset" });
});

afterAll(() => db.teardown());

createTestDatabase

import { createTestDatabase } from "hazo_testing";

const { adapter, baseUrl, teardown } = await createTestDatabase({
  mode: "sqlite",          // default — "docker" throws DockerNotSupported
  setupSqlFiles: [         // full-schema .sql files, applied in array order
    "node_modules/hazo_auth/db_setup_sqlite.sql",
    "node_modules/hazo_jobs/db_setup_sqlite.sql",
    "node_modules/hazo_notify/db_setup_sqlite.sql",
    "./migrations/my_app_schema.sql",
  ],
  migrations: [            // numbered-migration directories (NNNN_*.sql)
    "./migrations",
  ],
});

// adapter — AugmentedAdapter (real SqliteAdapter + insertOne/findOne helpers)
// baseUrl — always null (SQLite mode has no HTTP endpoint)
// teardown() — disposes the in-memory DB

await teardown();
  • Each call creates an independent in-memory database — safe for parallel test workers.
  • mode: "docker" throws DockerNotSupported (deferred to a future version).
  • createTestDatabaseSqlite(opts) is an explicit-name alias.
  • createTestDatabaseDocker() always throws DockerNotSupported.

Augmented adapter

adapter is a HazoConnectAdapter augmented with two helper methods:

adapter.insertOne(table, row)        // → T & { id: string }
adapter.findOne(table, where)        // → T | null

These are thin shims over createCrudService from hazo_connect. They handle UUID generation automatically. The underlying CrudService API is insert(row) → T[] and findOneBy(where) → T | null (no DbResult wrapper).


Auth factories

createTestUser

import { createTestUser } from "hazo_testing";

// Bare user — just a hazo_users row + session cookie (backward-compatible):
const user = await createTestUser(adapter, {
  email: "[email protected]",
  name: "Alice",               // optional — defaults to email prefix
  columnOverrides: { url_on_logon: "/home" },  // any nullable hazo_users column
});
// user.id            — generated UUID
// user.email         — "[email protected]"
// user.sessionCookie — ready "Cookie:" header string

// Permissioned user — seeds the full role/permission/scope chain in one call:
const admin = await createTestUser(adapter, {
  email: "[email protected]",
  role: "admin",                               // role name; auto-created if absent
  permissions: ["system.access", "file.upload"], // permission names; auto-created
  scopeId: "org-1",                            // scope id; auto-created if absent
  scopeName: "Acme Corp",                      // optional — defaults to scopeId
  scopeLevel: "firm",                          // optional — defaults to "firm"
});
// admin.roleId    — id of the hazo_roles row
// admin.roleName  — "admin"
// admin.permissions — ["system.access", "file.upload"]
// admin.scopeId   — "org-1"
  • Writes a real hazo_users row. Note: the column is email_address in the schema; the factory maps the email option automatically.
  • Mints a session cookie using HS256 JWT (same algorithm as hazo_auth) signed with process.env.JWT_SECRET.
  • When role, permissions, or scopeId are given, seeds the full chain across hazo_roles, hazo_user_roles, hazo_permissions, hazo_role_permissions, hazo_scopes, and hazo_user_scopes. All rows are reused-or-created, so multiple calls sharing the same role/scopeId in one DB are idempotent.
  • Bare calls (no chain opts) return only { id, email, sessionCookie } — unchanged from v0.2.x.

createTestScope

import { createTestScope } from "hazo_testing";

const scope = await createTestScope(adapter, {
  name: "Acme Corp",
  level: "firm",     // optional — defaults to "firm"; hazo_scopes.level is NOT NULL
  members: [],       // accepted but ignored in v0.1.0 (membership rows deferred)
});

// scope.id   — generated UUID
// scope.name — "Acme Corp"
  • Writes a real hazo_scopes row.
  • hazo_scopes requires level TEXT NOT NULL — always supply a value or accept the "firm" default.
  • hazo_scopes has no owner_user_id column. The owner_user_id option is accepted for future use but is not stored.

Wiring helpers

wireHazoJobs — zero external I/O

import { wireHazoJobs } from "hazo_testing";

const jobs = await wireHazoJobs(adapter);

const { jobId } = await jobs.submit({ type: "my-job", description: "test run", payload: { n: 1 } });

const worker = jobs.worker({ workerId: "w1", types: ["my-job"] });
await worker.run(async (job) => {
  const payload = JSON.parse(job.payload as string);  // payload is a raw JSON string
  worker.stop();                                       // stop from INSIDE the handler
});

const row = await jobs.get(jobId);
expect(row.status).toBe("completed");
  • Applies hazo_jobs DDL and creates a JobsClient against the test adapter.
  • Fully real, fully deterministic — pure DB rows and handler invocation.
  • worker.run() is a blocking poll loop — always call worker.stop() from inside the handler, not after run() awaits.
  • job.payload is a raw JSON string; call JSON.parse() inside the handler.
  • hazo_jobs DDL is loaded from the package root (db_setup_sqlite.sql) via import.meta.resolve because the file is not in the package exports map.
  • hazo_jobs/server is imported via dynamic import() to avoid a CJS/ESM module-load race under Jest's --experimental-vm-modules runner.

wireHazoNotify — real external sends when channels enabled

import { wireHazoNotify } from "hazo_testing";

// Inbox-only path — no credentials needed
const notify = await wireHazoNotify(adapter);
const result = await notify.dispatch({
  scope_id: scope.id,
  event_type: "test.event",
  recipient_user_ids: [user.id],
  in_app_text: "Hello",
  deep_link: "/dashboard",
  surfaces: ["inapp"],
  channels: { inapp: true },
  channel_payloads: {},
});

// Real email — requires ZEPTOMAIL_API_KEY
const notifyEmail = await wireHazoNotify(adapter, { channels: ["email"] });
  • Always auto-registers a no-op inapp stub channel so the inbox path works without credentials. Inbox table: hazo_notify_inbox.
  • Real email/telegram channels perform live I/O — skip-gate tests on env var presence.
  • __resetChannelRegistry is called automatically at the start of each wireHazoNotify call so tests can call it multiple times without "channel already registered" errors.
  • The hazo_notify_inbox, hazo_notify_channel_deliveries, hazo_notify_preferences, and template tables must exist in the test database — include hazo_notify/db_setup_sqlite.sql in setupSqlFiles.

wireHazoLlmApi — real billed calls

import { wireHazoLlmApi } from "hazo_testing";

const llm = await wireHazoLlmApi({ providers: ["gemini"], primary: "gemini" });

// Assertions check shape, not text (non-deterministic)
const res = await llm.run({ prompt: "Say hello in one word" }, "gemini");
expect(res.success).toBe(true);
expect(typeof res.text).toBe("string");
  • Registers the real Gemini/Qwen providers with API keys read from environment variables.
  • Provider constructors accept an empty api_key without throwing — errors only surface at call time. This lets you assert typeof llm.run === "function" without credentials.
  • Skip-gate tests that call llm.run(...) on GEMINI_API_KEY / QWEN_API_KEY.
  • Fragile internal path: clear_registry, register_provider, set_enabled_llms, set_primary_llm, GeminiProvider, and QwenProvider are not exported from hazo_llm_api/server. wireHazoLlmApi reaches them via import.meta.resolve("hazo_llm_api/server") → dist root → dynamic import of internal dist paths. This will break if hazo_llm_api restructures its dist. Track FR-001 in hazo_llm_api/design/feature_requests/FR-001-export-provider-registry.md.
  • initialize_llm_api is called with a stub hazo_connect (api_log: { enabled: false }) so no prompt_library.sqlite is written to disk. If you see that file appearing, add it to your consumer's .gitignore.

apiTestClient

import { apiTestClient } from "hazo_testing";

const client = apiTestClient("http://localhost:3000", {
  sessionCookie: user.sessionCookie,  // optional
  token: "bearer-token",              // optional — injects Authorization header
});

const { status, body } = await client.get("/api/items");
const { status: s2 } = await client.post("/api/items", { name: "test" });
const { status: s3 } = await client.patch("/api/items/1", { name: "updated" });
const { status: s4 } = await client.delete("/api/items/1");

// File upload
const { status: s5 } = await client.upload("/api/files", {
  file: Buffer.from("hello"),
  mimeType: "text/plain",
  fieldName: "file",          // optional, default "file"
  filename: "hello.txt",      // optional
});

// SSE streaming
for await (const chunk of client.stream("/api/stream", { prompt: "..." })) {
  process.stdout.write(chunk);
}

// Correlation ID from last response
console.log(client.lastCorrelationId);
  • Cookie jar is per-client instance and auto-rotates Set-Cookie headers.
  • Returns { status, body, headers } on every call. body is auto-parsed as JSON if Content-Type: application/json.

Fixtures

import { seedTestData, registerFixture } from "hazo_testing";

// Built-in fixtures
await seedTestData(adapter, { fixture: "empty_with_one_user" });
await seedTestData(adapter, { fixture: "small_dataset" });    // ~5 users / 2 scopes
await seedTestData(adapter, { fixture: "large_dataset" });    // 500 users / 50 scopes

// Register a custom fixture
registerFixture("my_fixture", async (adapter) => {
  const user = await createTestUser(adapter, { email: "[email protected]" });
  return { users: [user] };
});
await seedTestData(adapter, { fixture: "my_fixture" });
  • Built-in fixtures write auth/scope rows only (S6 neutrality — no app-domain rows).
  • large_dataset is 500 users / 50 scopes in v0.1.0 (the spec's 5 000/500 was scaled down for in-memory sql.js performance).

Jest preset

// jest.config.cjs
module.exports = require("hazo_testing/jest-preset")({
  aliasRoot: "<rootDir>/src",   // optional — sets @/... alias; pass false to disable
  setupFilesAfterEnv: [],       // optional — additional setup files
});
  • Sets testEnvironment: "jsdom", ts-jest transform with isolatedModules: true.
  • transformIgnorePatterns allows hazo_* packages to be transformed.
  • Injects polyfills (TextEncoder, structuredClone, fetch) via setupFiles.
  • Calls all registered reset hooks in afterEach via setupFilesAfterEnv.
  • testTimeout: 10 000 ms.
  • jest-preset.cjs is CommonJS (the package is type: "module" but Jest config must be CJS).

Jest must run with:

{ "test": "NODE_OPTIONS=--experimental-vm-modules jest" }

Without --experimental-vm-modules, ESM imports (hazo_notify, hazo_jobs, hazo_llm_api) will fail.


Vitest preset (0.2.0)

hazo_testing/vitest-preset is the Vitest counterpart to the Jest preset — an ESM config factory. Additive: the Jest preset and all v0.1.x exports are unchanged.

// vitest.config.ts
import { defineConfig } from "vitest/config";
import hazoTestingVitest from "hazo_testing/vitest-preset";

export default defineConfig(
  hazoTestingVitest({
    // alias: { ... },        // optional — extra resolve.alias entries
    // setupFiles: [ ... ],   // optional — additional setup files
  }),
);
  • test.environment: "node" — opt into jsdom per-file with a // @vitest-environment jsdom docblock.
  • setupFiles: ["hazo_testing/vitest-setup"] — runs registered reset hooks in afterEach (Vitest imports afterEach explicitly; it is not a global).
  • resolve.alias maps server-only → an empty stub so server modules import cleanly under Vitest.
  • server.deps.inline: [/hazo_/] — hazo_* packages transform through Vite.
  • testTimeout: 10 000 ms.
  • vitest-preset.mjs is ESM (Vitest reads config through Vite's ESM loader).

Install the optional peer: npm install --save-dev vitest.

renderWithIntlhazo_testing/render

import { renderWithIntl } from "hazo_testing/render";

const { getByText } = await renderWithIntl(<MyComponent />, {
  locale: "en",                 // default "en"
  messages: { Greeting: { hi: "Hello" } },  // default {}
});

Wraps ui in next-intl's NextIntlClientProvider and renders via @testing-library/react. Both are optional peers, resolved lazily.

expectNoAxeViolationshazo_testing/axe

import { expectNoAxeViolations } from "hazo_testing/axe";

await expectNoAxeViolations(container); // WCAG 2.0/2.1 A + AA by default

Throws a formatted error listing any axe-core violations. axe-core ships as a direct dependency.


Reset hooks

import { registerResetHook, runResetHooks } from "hazo_testing";

// Register a cleanup function to run after each test
registerResetHook(() => { myCache.clear(); });

// The preset calls runResetHooks() automatically in afterEach.
// __clearResetHooks() is available for test isolation if needed.

Optional sub-exports

hazo_testing/playwright

import { hazoPage } from "hazo_testing/playwright";

Provides @playwright/test fixtures. Requires the optional peer @playwright/test.

hazo_testing/nextjs

import { startNextServer } from "hazo_testing/nextjs";

const server = await startNextServer({ dir: "/path/to/app", port: 3999 });
// server.url — "http://127.0.0.1:3999"
await server.teardown();

Boots a Next.js dev/prod server in-process. Requires the optional peer next. Port 0 binds to a random available port.


Test plan

The package's own suite (src/**/__tests__) covers:

| Scenario | Tests | Creds needed | Runs green locally | |----------|-------|--------------|--------------------| | db_lifecycle (sqlite) | 2–3 | none | yes | | factories | 5 | none (JWT_SECRET defaulted) | yes | | real_io — jobs | 3 | none | yes | | real_io — notify | 2 | ZEPTOMAIL_API_KEY etc. | skip-gated | | real_io — llm | 2 | GEMINI_API_KEY/QWEN_API_KEY | skip-gated | | api_client | 5 | none (in-test handler) | yes | | fixtures | 4 | none | yes |

Skip-gated tests print a clear it.skip reason when credentials are absent.