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

Merge pull request #6 from malpercio-dev/claude/analyze-test-coverage-6nuSo

docs: analyze test coverage gaps and propose testing strategy

authored by

Malpercio and committed by
GitHub
5aeb4fec eae0f515

+1560 -5
+1
apps/appview/package.json
··· 8 8 "dev": "tsx watch --env-file=../../.env src/index.ts", 9 9 "start": "node dist/index.js", 10 10 "lint": "tsc --noEmit", 11 + "test": "vitest run", 11 12 "clean": "rm -rf dist", 12 13 "db:generate": "drizzle-kit generate", 13 14 "db:migrate": "drizzle-kit migrate"
+68
apps/appview/src/lib/__tests__/config.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + 3 + describe("loadConfig", () => { 4 + const originalEnv = { ...process.env }; 5 + 6 + beforeEach(() => { 7 + vi.resetModules(); 8 + }); 9 + 10 + afterEach(() => { 11 + process.env = { ...originalEnv }; 12 + }); 13 + 14 + async function loadConfig() { 15 + const mod = await import("../config.js"); 16 + return mod.loadConfig(); 17 + } 18 + 19 + it("returns default port 3000 when PORT is undefined", async () => { 20 + delete process.env.PORT; 21 + const config = await loadConfig(); 22 + expect(config.port).toBe(3000); 23 + }); 24 + 25 + it("parses PORT as an integer", async () => { 26 + process.env.PORT = "4000"; 27 + const config = await loadConfig(); 28 + expect(config.port).toBe(4000); 29 + expect(typeof config.port).toBe("number"); 30 + }); 31 + 32 + it("returns default PDS URL when PDS_URL is undefined", async () => { 33 + delete process.env.PDS_URL; 34 + const config = await loadConfig(); 35 + expect(config.pdsUrl).toBe("https://bsky.social"); 36 + }); 37 + 38 + it("uses provided environment variables", async () => { 39 + process.env.PORT = "5000"; 40 + process.env.FORUM_DID = "did:plc:test123"; 41 + process.env.PDS_URL = "https://my-pds.example.com"; 42 + process.env.DATABASE_URL = "postgres://localhost/testdb"; 43 + const config = await loadConfig(); 44 + expect(config.port).toBe(5000); 45 + expect(config.forumDid).toBe("did:plc:test123"); 46 + expect(config.pdsUrl).toBe("https://my-pds.example.com"); 47 + expect(config.databaseUrl).toBe("postgres://localhost/testdb"); 48 + }); 49 + 50 + it("returns empty string for forumDid when FORUM_DID is undefined", async () => { 51 + delete process.env.FORUM_DID; 52 + const config = await loadConfig(); 53 + expect(config.forumDid).toBe(""); 54 + }); 55 + 56 + it("returns empty string for databaseUrl when DATABASE_URL is undefined", async () => { 57 + delete process.env.DATABASE_URL; 58 + const config = await loadConfig(); 59 + expect(config.databaseUrl).toBe(""); 60 + }); 61 + 62 + it("returns NaN for port when PORT is empty string (?? does not catch empty strings)", async () => { 63 + process.env.PORT = ""; 64 + const config = await loadConfig(); 65 + // Documents a gap: ?? only catches null/undefined, not "" 66 + expect(config.port).toBeNaN(); 67 + }); 68 + });
+34
apps/appview/src/routes/__tests__/categories.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("GET /api/categories", () => { 8 + it("returns 200", async () => { 9 + const res = await app.request("/api/categories"); 10 + expect(res.status).toBe(200); 11 + }); 12 + 13 + it("returns an object with a categories array", async () => { 14 + const res = await app.request("/api/categories"); 15 + const body = await res.json(); 16 + expect(body).toHaveProperty("categories"); 17 + expect(Array.isArray(body.categories)).toBe(true); 18 + }); 19 + }); 20 + 21 + describe("GET /api/categories/:id/topics", () => { 22 + it("returns 200", async () => { 23 + const res = await app.request("/api/categories/123/topics"); 24 + expect(res.status).toBe(200); 25 + }); 26 + 27 + it("echoes the category id and returns a topics array", async () => { 28 + const res = await app.request("/api/categories/42/topics"); 29 + const body = await res.json(); 30 + expect(body).toHaveProperty("categoryId", "42"); 31 + expect(body).toHaveProperty("topics"); 32 + expect(Array.isArray(body.topics)).toBe(true); 33 + }); 34 + });
+23
apps/appview/src/routes/__tests__/forum.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("GET /api/forum", () => { 8 + it("returns 200", async () => { 9 + const res = await app.request("/api/forum"); 10 + expect(res.status).toBe(200); 11 + }); 12 + 13 + it("returns forum metadata with expected shape", async () => { 14 + const res = await app.request("/api/forum"); 15 + const body = await res.json(); 16 + expect(body).toHaveProperty("name"); 17 + expect(body).toHaveProperty("description"); 18 + expect(body).toHaveProperty("did"); 19 + expect(typeof body.name).toBe("string"); 20 + expect(typeof body.description).toBe("string"); 21 + expect(typeof body.did).toBe("string"); 22 + }); 23 + });
+28
apps/appview/src/routes/__tests__/health.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("GET /api/healthz", () => { 8 + it("returns 200 with ok status", async () => { 9 + const res = await app.request("/api/healthz"); 10 + expect(res.status).toBe(200); 11 + const body = await res.json(); 12 + expect(body).toEqual({ status: "ok", version: "0.1.0" }); 13 + }); 14 + 15 + it("returns application/json content type", async () => { 16 + const res = await app.request("/api/healthz"); 17 + expect(res.headers.get("content-type")).toContain("application/json"); 18 + }); 19 + }); 20 + 21 + describe("GET /api/healthz/ready", () => { 22 + it("returns 200 with ready status", async () => { 23 + const res = await app.request("/api/healthz/ready"); 24 + expect(res.status).toBe(200); 25 + const body = await res.json(); 26 + expect(body).toEqual({ status: "ready" }); 27 + }); 28 + });
+18
apps/appview/src/routes/__tests__/posts.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("POST /api/posts", () => { 8 + it("returns 501 not implemented", async () => { 9 + const res = await app.request("/api/posts", { method: "POST" }); 10 + expect(res.status).toBe(501); 11 + }); 12 + 13 + it("returns an error message", async () => { 14 + const res = await app.request("/api/posts", { method: "POST" }); 15 + const body = await res.json(); 16 + expect(body).toHaveProperty("error", "not implemented"); 17 + }); 18 + });
+25
apps/appview/src/routes/__tests__/routing.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("API routing", () => { 8 + it("returns 404 for unknown routes", async () => { 9 + const res = await app.request("/api/nonexistent"); 10 + expect(res.status).toBe(404); 11 + }); 12 + 13 + it("mounts all expected route prefixes", async () => { 14 + const routes = [ 15 + "/api/healthz", 16 + "/api/forum", 17 + "/api/categories", 18 + ]; 19 + 20 + for (const route of routes) { 21 + const res = await app.request(route); 22 + expect(res.status, `${route} should be reachable`).not.toBe(404); 23 + } 24 + }); 25 + });
+34
apps/appview/src/routes/__tests__/topics.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("GET /api/topics/:id", () => { 8 + it("returns 200", async () => { 9 + const res = await app.request("/api/topics/abc123"); 10 + expect(res.status).toBe(200); 11 + }); 12 + 13 + it("echoes the topic id and returns expected shape", async () => { 14 + const res = await app.request("/api/topics/abc123"); 15 + const body = await res.json(); 16 + expect(body).toHaveProperty("topicId", "abc123"); 17 + expect(body).toHaveProperty("post"); 18 + expect(body).toHaveProperty("replies"); 19 + expect(Array.isArray(body.replies)).toBe(true); 20 + }); 21 + }); 22 + 23 + describe("POST /api/topics", () => { 24 + it("returns 501 not implemented", async () => { 25 + const res = await app.request("/api/topics", { method: "POST" }); 26 + expect(res.status).toBe(501); 27 + }); 28 + 29 + it("returns an error message", async () => { 30 + const res = await app.request("/api/topics", { method: "POST" }); 31 + const body = await res.json(); 32 + expect(body).toHaveProperty("error", "not implemented"); 33 + }); 34 + });
+7
apps/appview/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + }, 7 + });
+1
apps/web/package.json
··· 8 8 "dev": "tsx watch --env-file=../../.env src/index.ts", 9 9 "start": "node dist/index.js", 10 10 "lint": "tsc --noEmit", 11 + "test": "vitest run", 11 12 "clean": "rm -rf dist" 12 13 }, 13 14 "dependencies": {
+74
apps/web/src/lib/__tests__/api.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + 3 + const mockFetch = vi.fn(); 4 + 5 + describe("fetchApi", () => { 6 + beforeEach(() => { 7 + vi.stubGlobal("fetch", mockFetch); 8 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 9 + vi.resetModules(); 10 + }); 11 + 12 + afterEach(() => { 13 + vi.unstubAllGlobals(); 14 + vi.unstubAllEnvs(); 15 + mockFetch.mockReset(); 16 + }); 17 + 18 + async function loadFetchApi() { 19 + const mod = await import("../api.js"); 20 + return mod.fetchApi; 21 + } 22 + 23 + it("calls the correct URL", async () => { 24 + mockFetch.mockResolvedValueOnce({ 25 + ok: true, 26 + json: () => Promise.resolve({ data: "test" }), 27 + }); 28 + 29 + const fetchApi = await loadFetchApi(); 30 + await fetchApi("/categories"); 31 + 32 + expect(mockFetch).toHaveBeenCalledOnce(); 33 + const calledUrl = mockFetch.mock.calls[0][0]; 34 + expect(calledUrl).toBe("http://localhost:3000/api/categories"); 35 + }); 36 + 37 + it("returns parsed JSON on success", async () => { 38 + const expected = { categories: [{ id: 1, name: "General" }] }; 39 + mockFetch.mockResolvedValueOnce({ 40 + ok: true, 41 + json: () => Promise.resolve(expected), 42 + }); 43 + 44 + const fetchApi = await loadFetchApi(); 45 + const result = await fetchApi("/categories"); 46 + expect(result).toEqual(expected); 47 + }); 48 + 49 + it("throws on non-ok response", async () => { 50 + mockFetch.mockResolvedValueOnce({ 51 + ok: false, 52 + status: 500, 53 + statusText: "Internal Server Error", 54 + }); 55 + 56 + const fetchApi = await loadFetchApi(); 57 + await expect(fetchApi("/fail")).rejects.toThrow( 58 + "AppView API error: 500 Internal Server Error" 59 + ); 60 + }); 61 + 62 + it("throws on 404 response", async () => { 63 + mockFetch.mockResolvedValueOnce({ 64 + ok: false, 65 + status: 404, 66 + statusText: "Not Found", 67 + }); 68 + 69 + const fetchApi = await loadFetchApi(); 70 + await expect(fetchApi("/missing")).rejects.toThrow( 71 + "AppView API error: 404 Not Found" 72 + ); 73 + }); 74 + });
+59
apps/web/src/lib/__tests__/config.test.ts
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + 3 + describe("loadConfig", () => { 4 + const originalEnv = { ...process.env }; 5 + 6 + beforeEach(() => { 7 + vi.resetModules(); 8 + }); 9 + 10 + afterEach(() => { 11 + process.env = { ...originalEnv }; 12 + }); 13 + 14 + async function loadConfig() { 15 + const mod = await import("../config.js"); 16 + return mod.loadConfig(); 17 + } 18 + 19 + it("returns default port 3001 when PORT is undefined", async () => { 20 + delete process.env.PORT; 21 + const config = await loadConfig(); 22 + expect(config.port).toBe(3001); 23 + }); 24 + 25 + it("parses PORT as an integer", async () => { 26 + process.env.PORT = "8080"; 27 + const config = await loadConfig(); 28 + expect(config.port).toBe(8080); 29 + expect(typeof config.port).toBe("number"); 30 + }); 31 + 32 + it("returns default appview URL when APPVIEW_URL is undefined", async () => { 33 + delete process.env.APPVIEW_URL; 34 + const config = await loadConfig(); 35 + expect(config.appviewUrl).toBe("http://localhost:3000"); 36 + }); 37 + 38 + it("uses provided environment variables", async () => { 39 + process.env.PORT = "9000"; 40 + process.env.APPVIEW_URL = "https://api.atbb.space"; 41 + const config = await loadConfig(); 42 + expect(config.port).toBe(9000); 43 + expect(config.appviewUrl).toBe("https://api.atbb.space"); 44 + }); 45 + 46 + it("returns NaN for port when PORT is empty string (?? does not catch empty strings)", async () => { 47 + process.env.PORT = ""; 48 + const config = await loadConfig(); 49 + // Documents a gap: ?? only catches null/undefined, not "" 50 + expect(config.port).toBeNaN(); 51 + }); 52 + 53 + it("returns empty string for appviewUrl when APPVIEW_URL is empty string", async () => { 54 + process.env.APPVIEW_URL = ""; 55 + const config = await loadConfig(); 56 + // Documents a gap: ?? only catches null/undefined, not "" 57 + expect(config.appviewUrl).toBe(""); 58 + }); 59 + });
+7
apps/web/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + }, 7 + });
+259
docs/test-coverage-analysis.md
··· 1 + # Test Coverage Analysis 2 + 3 + ## Current State: No Tests Exist 4 + 5 + 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. 6 + 7 + --- 8 + 9 + ## Recommended Test Framework Setup 10 + 11 + **Vitest** is the best fit for this project: 12 + - Native ESM support (the repo uses `"type": "module"` everywhere) 13 + - Built-in TypeScript support via `tsx`/`esbuild` (no separate ts-jest config) 14 + - Workspace-aware — can share a root config while per-package configs override as needed 15 + - Fast, with watch mode out of the box 16 + 17 + ### Infrastructure needed 18 + 19 + 1. Install `vitest` as a root devDependency 20 + 2. Add a root `vitest.workspace.ts` pointing at each package 21 + 3. Add `"test": "vitest run"` scripts to each package's `package.json` 22 + 4. Add a `"test"` task to `turbo.json` (with `dependsOn: ["^build"]` since appview/web depend on lexicon types) 23 + 5. Add `pnpm test` as a root script 24 + 25 + --- 26 + 27 + ## Package-by-Package Gaps and Recommendations 28 + 29 + ### 1. `@atbb/appview` — API Server (highest priority) 30 + 31 + 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. 32 + 33 + #### a) Route handler tests (high value) 34 + 35 + **Files:** `src/routes/health.ts`, `src/routes/forum.ts`, `src/routes/categories.ts`, `src/routes/topics.ts`, `src/routes/posts.ts` 36 + 37 + **What to test:** 38 + - `GET /api/healthz` returns `200` with `{ status: "ok", version: "0.1.0" }` 39 + - `GET /api/healthz/ready` returns `200` with `{ status: "ready" }` 40 + - `GET /api/forum` returns `200` with the expected forum shape 41 + - `GET /api/categories` returns `200` with `{ categories: [] }` 42 + - `GET /api/categories/:id/topics` returns `200` and echoes the `id` param 43 + - `POST /api/topics` returns `501` (not implemented) 44 + - `POST /api/posts` returns `501` (not implemented) 45 + 46 + **How:** Use Hono's built-in `app.request()` test helper — no HTTP server needed: 47 + ```ts 48 + import { describe, it, expect } from "vitest"; 49 + import { apiRoutes } from "../src/routes/index.js"; 50 + import { Hono } from "hono"; 51 + 52 + const app = new Hono().route("/api", apiRoutes); 53 + 54 + describe("GET /api/healthz", () => { 55 + it("returns ok status", async () => { 56 + const res = await app.request("/api/healthz"); 57 + expect(res.status).toBe(200); 58 + expect(await res.json()).toEqual({ status: "ok", version: "0.1.0" }); 59 + }); 60 + }); 61 + ``` 62 + 63 + **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. 64 + 65 + #### b) Config loading tests (medium value) 66 + 67 + **File:** `src/lib/config.ts` 68 + 69 + **What to test:** 70 + - Returns correct defaults when env vars are absent (`PORT` defaults to `3000`, `PDS_URL` defaults to `https://bsky.social`) 71 + - Parses `PORT` as an integer (not a string) 72 + - Returns provided env var values when set 73 + - Handles `PORT` set to a non-numeric string (currently `parseInt` would return `NaN` — should this throw?) 74 + 75 + **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. 76 + 77 + #### c) Database schema tests (medium value) 78 + 79 + **File:** `src/db/schema.ts` 80 + 81 + **What to test:** 82 + - Schema definitions export the expected table names 83 + - Column types match expectations (e.g., `posts.deleted` defaults to `false`) 84 + - Foreign key references are correct (`posts.did` → `users.did`, `posts.rootPostId` → `posts.id`, etc.) 85 + - Index names are correct and unique constraints are in place 86 + 87 + **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. 88 + 89 + **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. 90 + 91 + #### d) Database integration tests (high value, but requires infrastructure) 92 + 93 + **What to test:** 94 + - Insert/select/update/delete for each table 95 + - Foreign key constraints are enforced (e.g., inserting a post with a non-existent `did` fails) 96 + - Unique index violations behave as expected 97 + - The `createDb()` factory produces a working Drizzle client 98 + 99 + **How:** Use a test PostgreSQL instance. Options: 100 + - **Testcontainers** (Docker-based, spins up a real Postgres per test suite) 101 + - **pg-mem** (in-memory Postgres emulator, faster but not 100% compatible) 102 + - A shared test database with transaction rollback between tests 103 + 104 + **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. 105 + 106 + #### e) AT Protocol agent factory test (low value now, higher later) 107 + 108 + **File:** `src/lib/atproto.ts` 109 + 110 + 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: 111 + - Agent is created with the correct service URL 112 + - Authentication errors are handled gracefully 113 + - Record write/read operations produce expected AT URI formats 114 + 115 + --- 116 + 117 + ### 2. `@atbb/web` — Server-Rendered Web UI 118 + 119 + #### a) API client tests (high value) 120 + 121 + **File:** `src/lib/api.ts` 122 + 123 + **What to test:** 124 + - `fetchApi("/categories")` calls the correct URL (`${appviewUrl}/api/categories`) 125 + - Throws an `Error` with status code and status text on non-2xx responses 126 + - Returns parsed JSON on success 127 + - Handles network failures (fetch throws) 128 + 129 + **How:** Mock `global.fetch` with `vi.fn()` or use `msw` (Mock Service Worker): 130 + ```ts 131 + import { describe, it, expect, vi, beforeEach } from "vitest"; 132 + 133 + // Mock fetch globally 134 + const mockFetch = vi.fn(); 135 + vi.stubGlobal("fetch", mockFetch); 136 + 137 + describe("fetchApi", () => { 138 + it("throws on non-ok response", async () => { 139 + mockFetch.mockResolvedValueOnce({ 140 + ok: false, status: 500, statusText: "Internal Server Error", 141 + }); 142 + const { fetchApi } = await import("../src/lib/api.js"); 143 + await expect(fetchApi("/test")).rejects.toThrow("AppView API error: 500"); 144 + }); 145 + }); 146 + ``` 147 + 148 + **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. 149 + 150 + #### b) JSX component / layout tests (medium value) 151 + 152 + **File:** `src/layouts/base.tsx`, `src/routes/home.tsx` 153 + 154 + **What to test:** 155 + - `BaseLayout` renders valid HTML with the provided title 156 + - `BaseLayout` uses the default title "atBB Forum" when none is provided 157 + - `BaseLayout` includes the HTMX script tag 158 + - Home route returns `200` with `text/html` content type 159 + - Home page includes "Welcome to atBB" heading 160 + 161 + **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. 162 + 163 + #### c) Config loading tests (low-medium value) 164 + 165 + **File:** `src/lib/config.ts` 166 + 167 + Same pattern as the appview config tests — verify defaults, parsing, and presence of required values. 168 + 169 + --- 170 + 171 + ### 3. `@atbb/lexicon` — Lexicon Definitions 172 + 173 + #### a) YAML-to-JSON build script tests (medium value) 174 + 175 + **File:** `scripts/build.ts` 176 + 177 + **What to test:** 178 + - Each YAML file in `lexicons/` produces valid JSON 179 + - Output JSON matches the expected Lexicon schema structure (has `lexicon`, `id`, `defs` fields) 180 + - No duplicate lexicon IDs across files 181 + - The `id` field in each lexicon matches its file path (e.g., `space/atbb/post.yaml` has `id: "space.atbb.post"`) 182 + 183 + **How:** Rather than testing the build script directly (it's I/O-heavy), write validation tests that run against the YAML source files: 184 + ```ts 185 + import { parse } from "yaml"; 186 + import { readFileSync } from "fs"; 187 + import { glob } from "glob"; 188 + 189 + describe("lexicon definitions", () => { 190 + const files = glob.sync("**/*.yaml", { cwd: "lexicons" }); 191 + 192 + it.each(files)("%s has a valid lexicon structure", (file) => { 193 + const content = readFileSync(`lexicons/${file}`, "utf-8"); 194 + const parsed = parse(content); 195 + expect(parsed).toHaveProperty("lexicon", 1); 196 + expect(parsed).toHaveProperty("id"); 197 + expect(parsed).toHaveProperty("defs"); 198 + }); 199 + }); 200 + ``` 201 + 202 + **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. 203 + 204 + #### b) Schema contract tests (high value) 205 + 206 + **What to test:** 207 + - `space.atbb.post` has `text` as a required string field 208 + - `space.atbb.post` has optional `reply` with `root` and `parent` refs 209 + - `space.atbb.forum.forum` uses `key: literal:self` 210 + - `space.atbb.forum.category` uses `key: tid` 211 + - All `strongRef` usages have both `uri` and `cid` fields 212 + - `knownValues` are used (not `enum`) for extensible fields like `modAction.action` 213 + 214 + **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. 215 + 216 + --- 217 + 218 + ### 4. `@atbb/spike` — PDS Integration Script 219 + 220 + The spike is a manual integration test. It doesn't need unit tests itself, but: 221 + 222 + #### Extractable test utilities (medium value) 223 + 224 + The spike contains reusable patterns for: 225 + - Authenticating with a PDS 226 + - Creating/reading/deleting AT Protocol records 227 + - Generating TIDs 228 + 229 + These should be extracted into a shared test utility module (e.g., `packages/test-utils/`) that integration tests across the monorepo can use. 230 + 231 + --- 232 + 233 + ## Priority Matrix 234 + 235 + | Priority | Area | Package | Effort | Impact | 236 + |----------|------|---------|--------|--------| 237 + | **P0** | Test infrastructure setup (vitest, turbo task, CI) | root | Low | Unblocks everything | 238 + | **P0** | Appview route handler tests | appview | Low | Catches regressions as stubs are implemented | 239 + | **P1** | Web API client tests (`fetchApi`) | web | Low | Validates the only web→appview boundary | 240 + | **P1** | Lexicon schema contract tests | lexicon | Low | Protects the AT Protocol API surface | 241 + | **P1** | Config loading tests (both packages) | appview, web | Low | Documents defaults, catches parse bugs | 242 + | **P2** | Database schema unit tests | appview | Medium | Catches accidental schema changes | 243 + | **P2** | JSX layout/component tests | web | Medium | Ensures correct HTML output | 244 + | **P2** | Lexicon build script validation | lexicon | Low | Catches YAML/JSON conversion issues | 245 + | **P3** | Database integration tests | appview | High | Requires Postgres test infra (Docker/testcontainers) | 246 + | **P3** | AT Protocol integration tests | appview | High | Requires PDS test instance or mock | 247 + | **P3** | Extract spike utilities into shared test-utils | spike | Medium | Enables reuse across integration tests | 248 + 249 + --- 250 + 251 + ## Suggested Implementation Order 252 + 253 + 1. **Set up vitest** at the root + per-package, add `test` task to turbo.json 254 + 2. **Appview route tests** — quick wins since Hono has a built-in test helper and the routes are simple right now 255 + 3. **Lexicon contract tests** — validate YAML schema structure to protect the AT Protocol API 256 + 4. **Web `fetchApi` tests** — mock fetch, verify URL construction and error handling 257 + 5. **Config tests** for both packages — small but catches real bugs 258 + 6. **Database schema tests** — assert on Drizzle metadata objects 259 + 7. **Database integration tests** — add testcontainers or similar once there are real queries to test
+4 -2
package.json
··· 6 6 "build": "turbo run build", 7 7 "dev": "turbo run dev", 8 8 "lint": "turbo run lint", 9 - "clean": "turbo run clean" 9 + "clean": "turbo run clean", 10 + "test": "turbo run test" 10 11 }, 11 12 "devDependencies": { 12 13 "turbo": "^2.4.0", 13 - "typescript": "^5.7.0" 14 + "typescript": "^5.7.0", 15 + "vitest": "^4.0.18" 14 16 } 15 17 }
+2 -1
packages/db/package.json
··· 18 18 "scripts": { 19 19 "build": "tsc", 20 20 "lint": "tsc --noEmit", 21 - "clean": "rm -rf dist" 21 + "clean": "rm -rf dist", 22 + "test": "vitest run" 22 23 }, 23 24 "dependencies": { 24 25 "drizzle-orm": "^0.45.1",
+178
packages/db/src/__tests__/schema.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { getTableName, getTableColumns } from "drizzle-orm"; 3 + import { 4 + forums, 5 + categories, 6 + users, 7 + memberships, 8 + posts, 9 + modActions, 10 + } from "../schema.js"; 11 + 12 + describe("database schema", () => { 13 + describe("forums table", () => { 14 + it("has the correct table name", () => { 15 + expect(getTableName(forums)).toBe("forums"); 16 + }); 17 + 18 + it("has expected columns", () => { 19 + const cols = getTableColumns(forums); 20 + expect(cols).toHaveProperty("id"); 21 + expect(cols).toHaveProperty("did"); 22 + expect(cols).toHaveProperty("rkey"); 23 + expect(cols).toHaveProperty("cid"); 24 + expect(cols).toHaveProperty("name"); 25 + expect(cols).toHaveProperty("description"); 26 + expect(cols).toHaveProperty("indexedAt"); 27 + }); 28 + 29 + it("has did and rkey as not-null", () => { 30 + const cols = getTableColumns(forums); 31 + expect(cols.did.notNull).toBe(true); 32 + expect(cols.rkey.notNull).toBe(true); 33 + }); 34 + }); 35 + 36 + describe("categories table", () => { 37 + it("has the correct table name", () => { 38 + expect(getTableName(categories)).toBe("categories"); 39 + }); 40 + 41 + it("has expected columns", () => { 42 + const cols = getTableColumns(categories); 43 + expect(cols).toHaveProperty("id"); 44 + expect(cols).toHaveProperty("did"); 45 + expect(cols).toHaveProperty("rkey"); 46 + expect(cols).toHaveProperty("cid"); 47 + expect(cols).toHaveProperty("name"); 48 + expect(cols).toHaveProperty("description"); 49 + expect(cols).toHaveProperty("slug"); 50 + expect(cols).toHaveProperty("sortOrder"); 51 + expect(cols).toHaveProperty("forumId"); 52 + expect(cols).toHaveProperty("createdAt"); 53 + expect(cols).toHaveProperty("indexedAt"); 54 + }); 55 + }); 56 + 57 + describe("users table", () => { 58 + it("has the correct table name", () => { 59 + expect(getTableName(users)).toBe("users"); 60 + }); 61 + 62 + it("uses did as primary key", () => { 63 + const cols = getTableColumns(users); 64 + expect(cols.did.primary).toBe(true); 65 + }); 66 + 67 + it("has handle as optional", () => { 68 + const cols = getTableColumns(users); 69 + expect(cols.handle.notNull).toBe(false); 70 + }); 71 + }); 72 + 73 + describe("memberships table", () => { 74 + it("has the correct table name", () => { 75 + expect(getTableName(memberships)).toBe("memberships"); 76 + }); 77 + 78 + it("has expected columns", () => { 79 + const cols = getTableColumns(memberships); 80 + expect(cols).toHaveProperty("id"); 81 + expect(cols).toHaveProperty("did"); 82 + expect(cols).toHaveProperty("rkey"); 83 + expect(cols).toHaveProperty("cid"); 84 + expect(cols).toHaveProperty("forumId"); 85 + expect(cols).toHaveProperty("forumUri"); 86 + expect(cols).toHaveProperty("role"); 87 + expect(cols).toHaveProperty("roleUri"); 88 + expect(cols).toHaveProperty("joinedAt"); 89 + expect(cols).toHaveProperty("createdAt"); 90 + expect(cols).toHaveProperty("indexedAt"); 91 + }); 92 + 93 + it("has did and forumUri as not-null", () => { 94 + const cols = getTableColumns(memberships); 95 + expect(cols.did.notNull).toBe(true); 96 + expect(cols.forumUri.notNull).toBe(true); 97 + }); 98 + }); 99 + 100 + describe("posts table", () => { 101 + it("has the correct table name", () => { 102 + expect(getTableName(posts)).toBe("posts"); 103 + }); 104 + 105 + it("has expected columns for the unified post model", () => { 106 + const cols = getTableColumns(posts); 107 + expect(cols).toHaveProperty("id"); 108 + expect(cols).toHaveProperty("did"); 109 + expect(cols).toHaveProperty("rkey"); 110 + expect(cols).toHaveProperty("cid"); 111 + expect(cols).toHaveProperty("text"); 112 + expect(cols).toHaveProperty("forumUri"); 113 + expect(cols).toHaveProperty("rootPostId"); 114 + expect(cols).toHaveProperty("parentPostId"); 115 + expect(cols).toHaveProperty("rootUri"); 116 + expect(cols).toHaveProperty("parentUri"); 117 + expect(cols).toHaveProperty("createdAt"); 118 + expect(cols).toHaveProperty("indexedAt"); 119 + expect(cols).toHaveProperty("deleted"); 120 + }); 121 + 122 + it("has text as not-null", () => { 123 + const cols = getTableColumns(posts); 124 + expect(cols.text.notNull).toBe(true); 125 + }); 126 + 127 + it("has deleted defaulting to false", () => { 128 + const cols = getTableColumns(posts); 129 + expect(cols.deleted.notNull).toBe(true); 130 + expect(cols.deleted.hasDefault).toBe(true); 131 + }); 132 + 133 + it("has rootPostId and parentPostId as nullable (topics have no parent)", () => { 134 + const cols = getTableColumns(posts); 135 + expect(cols.rootPostId.notNull).toBe(false); 136 + expect(cols.parentPostId.notNull).toBe(false); 137 + }); 138 + }); 139 + 140 + describe("modActions table", () => { 141 + it("has the correct table name", () => { 142 + expect(getTableName(modActions)).toBe("mod_actions"); 143 + }); 144 + 145 + it("has expected columns", () => { 146 + const cols = getTableColumns(modActions); 147 + expect(cols).toHaveProperty("id"); 148 + expect(cols).toHaveProperty("did"); 149 + expect(cols).toHaveProperty("rkey"); 150 + expect(cols).toHaveProperty("cid"); 151 + expect(cols).toHaveProperty("action"); 152 + expect(cols).toHaveProperty("subjectDid"); 153 + expect(cols).toHaveProperty("subjectPostUri"); 154 + expect(cols).toHaveProperty("forumId"); 155 + expect(cols).toHaveProperty("reason"); 156 + expect(cols).toHaveProperty("createdBy"); 157 + expect(cols).toHaveProperty("expiresAt"); 158 + expect(cols).toHaveProperty("createdAt"); 159 + expect(cols).toHaveProperty("indexedAt"); 160 + }); 161 + 162 + it("has action and createdBy as not-null", () => { 163 + const cols = getTableColumns(modActions); 164 + expect(cols.action.notNull).toBe(true); 165 + expect(cols.createdBy.notNull).toBe(true); 166 + }); 167 + }); 168 + 169 + describe("all tables export correctly", () => { 170 + it("exports six tables", () => { 171 + const tables = [forums, categories, users, memberships, posts, modActions]; 172 + expect(tables).toHaveLength(6); 173 + tables.forEach((table) => { 174 + expect(table).toBeDefined(); 175 + }); 176 + }); 177 + }); 178 + });
+7
packages/db/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + }, 7 + });
+97
packages/lexicon/__tests__/lexicons.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { readFileSync } from "node:fs"; 3 + import { join } from "node:path"; 4 + import { parse as parseYaml } from "yaml"; 5 + import { globSync } from "glob"; 6 + 7 + const LEXICONS_DIR = join(import.meta.dirname, "..", "lexicons"); 8 + const yamlFiles = globSync("**/*.yaml", { cwd: LEXICONS_DIR }); 9 + 10 + describe("lexicon definitions", () => { 11 + it("finds at least one lexicon file", () => { 12 + expect(yamlFiles.length).toBeGreaterThan(0); 13 + }); 14 + 15 + describe.each(yamlFiles)("%s", (file) => { 16 + const content = readFileSync(join(LEXICONS_DIR, file), "utf-8"); 17 + const parsed = parseYaml(content); 18 + 19 + it("has lexicon version 1", () => { 20 + expect(parsed).toHaveProperty("lexicon", 1); 21 + }); 22 + 23 + it("has an id field", () => { 24 + expect(parsed).toHaveProperty("id"); 25 + expect(typeof parsed.id).toBe("string"); 26 + }); 27 + 28 + it("has a defs object", () => { 29 + expect(parsed).toHaveProperty("defs"); 30 + expect(typeof parsed.defs).toBe("object"); 31 + }); 32 + 33 + it("has an id that matches the file path", () => { 34 + // space/atbb/post.yaml -> space.atbb.post 35 + const expectedId = file.replace(/\.yaml$/, "").replace(/\//g, "."); 36 + expect(parsed.id).toBe(expectedId); 37 + }); 38 + }); 39 + }); 40 + 41 + describe("lexicon uniqueness", () => { 42 + it("has no duplicate lexicon ids", () => { 43 + const ids = yamlFiles.map((file) => { 44 + const content = readFileSync(join(LEXICONS_DIR, file), "utf-8"); 45 + return parseYaml(content).id; 46 + }); 47 + const unique = new Set(ids); 48 + expect(unique.size).toBe(ids.length); 49 + }); 50 + }); 51 + 52 + describe("record key conventions", () => { 53 + const recordLexicons = yamlFiles 54 + .map((file) => { 55 + const content = readFileSync(join(LEXICONS_DIR, file), "utf-8"); 56 + return { file, parsed: parseYaml(content) }; 57 + }) 58 + .filter(({ parsed }) => parsed.defs?.main?.type === "record"); 59 + 60 + it.each(recordLexicons.map(({ file, parsed }) => [file, parsed]))( 61 + "%s has a valid record key type", 62 + (_file, parsed) => { 63 + const key = parsed.defs.main.key; 64 + expect(["tid", "literal:self"]).toContain(key); 65 + } 66 + ); 67 + }); 68 + 69 + describe("extensible fields use knownValues", () => { 70 + it("modAction.action uses knownValues, not enum", () => { 71 + const content = readFileSync( 72 + join(LEXICONS_DIR, "space/atbb/modAction.yaml"), 73 + "utf-8" 74 + ); 75 + const parsed = parseYaml(content); 76 + const actionField = 77 + parsed.defs.main.record?.properties?.action; 78 + expect(actionField).toBeDefined(); 79 + expect(actionField).not.toHaveProperty("enum"); 80 + expect(actionField).toHaveProperty("knownValues"); 81 + }); 82 + }); 83 + 84 + describe("strongRef definitions", () => { 85 + it("strongRef has uri and cid fields", () => { 86 + const content = readFileSync( 87 + join(LEXICONS_DIR, "com/atproto/repo/strongRef.yaml"), 88 + "utf-8" 89 + ); 90 + const parsed = parseYaml(content); 91 + const mainDef = parsed.defs.main; 92 + expect(mainDef.properties).toHaveProperty("uri"); 93 + expect(mainDef.properties).toHaveProperty("cid"); 94 + expect(mainDef.required).toContain("uri"); 95 + expect(mainDef.required).toContain("cid"); 96 + }); 97 + });
+5 -2
packages/lexicon/package.json
··· 13 13 "build": "pnpm run build:json && pnpm run build:types", 14 14 "build:json": "tsx scripts/build.ts", 15 15 "build:types": "bash -c 'shopt -s globstar && lex gen-api --yes ./dist/types ./dist/json/**/*.json'", 16 + "test": "vitest run", 16 17 "clean": "rm -rf dist" 17 18 }, 18 19 "devDependencies": { 19 20 "@atproto/lex-cli": "^0.5.0", 21 + "@types/node": "^22.0.0", 22 + "glob": "^11.0.0", 20 23 "tsx": "^4.0.0", 21 - "yaml": "^2.7.0", 22 - "glob": "^11.0.0" 24 + "typescript": "^5.7.0", 25 + "yaml": "^2.7.0" 23 26 } 24 27 }
+7
packages/lexicon/vitest.config.ts
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + }, 7 + });
+611
pnpm-lock.yaml
··· 14 14 typescript: 15 15 specifier: ^5.7.0 16 16 version: 5.9.3 17 + vitest: 18 + specifier: ^4.0.18 19 + version: 4.0.18(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 17 20 18 21 apps/appview: 19 22 dependencies: ··· 92 95 '@atproto/lex-cli': 93 96 specifier: ^0.5.0 94 97 version: 0.5.7 98 + '@types/node': 99 + specifier: ^22.0.0 100 + version: 22.19.9 95 101 glob: 96 102 specifier: ^11.0.0 97 103 version: 11.1.0 98 104 tsx: 99 105 specifier: ^4.0.0 100 106 version: 4.21.0 107 + typescript: 108 + specifier: ^5.7.0 109 + version: 5.9.3 101 110 yaml: 102 111 specifier: ^2.7.0 103 112 version: 2.8.2 ··· 631 640 resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} 632 641 engines: {node: '>=18'} 633 642 643 + '@jridgewell/sourcemap-codec@1.5.5': 644 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 645 + 634 646 '@nodelib/fs.scandir@2.1.5': 635 647 resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 636 648 engines: {node: '>= 8'} ··· 643 655 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 644 656 engines: {node: '>= 8'} 645 657 658 + '@rollup/rollup-android-arm-eabi@4.57.1': 659 + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} 660 + cpu: [arm] 661 + os: [android] 662 + 663 + '@rollup/rollup-android-arm64@4.57.1': 664 + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} 665 + cpu: [arm64] 666 + os: [android] 667 + 668 + '@rollup/rollup-darwin-arm64@4.57.1': 669 + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} 670 + cpu: [arm64] 671 + os: [darwin] 672 + 673 + '@rollup/rollup-darwin-x64@4.57.1': 674 + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} 675 + cpu: [x64] 676 + os: [darwin] 677 + 678 + '@rollup/rollup-freebsd-arm64@4.57.1': 679 + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} 680 + cpu: [arm64] 681 + os: [freebsd] 682 + 683 + '@rollup/rollup-freebsd-x64@4.57.1': 684 + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} 685 + cpu: [x64] 686 + os: [freebsd] 687 + 688 + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': 689 + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} 690 + cpu: [arm] 691 + os: [linux] 692 + 693 + '@rollup/rollup-linux-arm-musleabihf@4.57.1': 694 + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} 695 + cpu: [arm] 696 + os: [linux] 697 + 698 + '@rollup/rollup-linux-arm64-gnu@4.57.1': 699 + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} 700 + cpu: [arm64] 701 + os: [linux] 702 + 703 + '@rollup/rollup-linux-arm64-musl@4.57.1': 704 + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} 705 + cpu: [arm64] 706 + os: [linux] 707 + 708 + '@rollup/rollup-linux-loong64-gnu@4.57.1': 709 + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} 710 + cpu: [loong64] 711 + os: [linux] 712 + 713 + '@rollup/rollup-linux-loong64-musl@4.57.1': 714 + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} 715 + cpu: [loong64] 716 + os: [linux] 717 + 718 + '@rollup/rollup-linux-ppc64-gnu@4.57.1': 719 + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} 720 + cpu: [ppc64] 721 + os: [linux] 722 + 723 + '@rollup/rollup-linux-ppc64-musl@4.57.1': 724 + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} 725 + cpu: [ppc64] 726 + os: [linux] 727 + 728 + '@rollup/rollup-linux-riscv64-gnu@4.57.1': 729 + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} 730 + cpu: [riscv64] 731 + os: [linux] 732 + 733 + '@rollup/rollup-linux-riscv64-musl@4.57.1': 734 + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} 735 + cpu: [riscv64] 736 + os: [linux] 737 + 738 + '@rollup/rollup-linux-s390x-gnu@4.57.1': 739 + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} 740 + cpu: [s390x] 741 + os: [linux] 742 + 743 + '@rollup/rollup-linux-x64-gnu@4.57.1': 744 + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} 745 + cpu: [x64] 746 + os: [linux] 747 + 748 + '@rollup/rollup-linux-x64-musl@4.57.1': 749 + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} 750 + cpu: [x64] 751 + os: [linux] 752 + 753 + '@rollup/rollup-openbsd-x64@4.57.1': 754 + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} 755 + cpu: [x64] 756 + os: [openbsd] 757 + 758 + '@rollup/rollup-openharmony-arm64@4.57.1': 759 + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} 760 + cpu: [arm64] 761 + os: [openharmony] 762 + 763 + '@rollup/rollup-win32-arm64-msvc@4.57.1': 764 + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} 765 + cpu: [arm64] 766 + os: [win32] 767 + 768 + '@rollup/rollup-win32-ia32-msvc@4.57.1': 769 + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} 770 + cpu: [ia32] 771 + os: [win32] 772 + 773 + '@rollup/rollup-win32-x64-gnu@4.57.1': 774 + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} 775 + cpu: [x64] 776 + os: [win32] 777 + 778 + '@rollup/rollup-win32-x64-msvc@4.57.1': 779 + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} 780 + cpu: [x64] 781 + os: [win32] 782 + 783 + '@standard-schema/spec@1.1.0': 784 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 785 + 646 786 '@ts-morph/common@0.17.0': 647 787 resolution: {integrity: sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g==} 648 788 789 + '@types/chai@5.2.3': 790 + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 791 + 792 + '@types/deep-eql@4.0.2': 793 + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 794 + 795 + '@types/estree@1.0.8': 796 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 797 + 649 798 '@types/node@22.19.9': 650 799 resolution: {integrity: sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==} 651 800 801 + '@vitest/expect@4.0.18': 802 + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} 803 + 804 + '@vitest/mocker@4.0.18': 805 + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} 806 + peerDependencies: 807 + msw: ^2.4.9 808 + vite: ^6.0.0 || ^7.0.0-0 809 + peerDependenciesMeta: 810 + msw: 811 + optional: true 812 + vite: 813 + optional: true 814 + 815 + '@vitest/pretty-format@4.0.18': 816 + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} 817 + 818 + '@vitest/runner@4.0.18': 819 + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} 820 + 821 + '@vitest/snapshot@4.0.18': 822 + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} 823 + 824 + '@vitest/spy@4.0.18': 825 + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} 826 + 827 + '@vitest/utils@4.0.18': 828 + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} 829 + 652 830 ansi-styles@4.3.0: 653 831 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 654 832 engines: {node: '>=8'} 655 833 834 + assertion-error@2.0.1: 835 + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 836 + engines: {node: '>=12'} 837 + 656 838 await-lock@2.2.2: 657 839 resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 658 840 ··· 668 850 669 851 buffer-from@1.1.2: 670 852 resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 853 + 854 + chai@6.2.2: 855 + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} 856 + engines: {node: '>=18'} 671 857 672 858 chalk@4.1.2: 673 859 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} ··· 796 982 sqlite3: 797 983 optional: true 798 984 985 + es-module-lexer@1.7.0: 986 + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 987 + 799 988 esbuild-register@3.6.0: 800 989 resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} 801 990 peerDependencies: ··· 816 1005 engines: {node: '>=18'} 817 1006 hasBin: true 818 1007 1008 + estree-walker@3.0.3: 1009 + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 1010 + 1011 + expect-type@1.3.0: 1012 + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 1013 + engines: {node: '>=12.0.0'} 1014 + 819 1015 fast-glob@3.3.3: 820 1016 resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 821 1017 engines: {node: '>=8.6.0'} 822 1018 823 1019 fastq@1.20.1: 824 1020 resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} 1021 + 1022 + fdir@6.5.0: 1023 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 1024 + engines: {node: '>=12.0.0'} 1025 + peerDependencies: 1026 + picomatch: ^3 || ^4 1027 + peerDependenciesMeta: 1028 + picomatch: 1029 + optional: true 825 1030 826 1031 fill-range@7.1.1: 827 1032 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} ··· 883 1088 resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} 884 1089 engines: {node: 20 || >=22} 885 1090 1091 + magic-string@0.30.21: 1092 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1093 + 886 1094 merge2@1.4.1: 887 1095 resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 888 1096 engines: {node: '>= 8'} ··· 914 1122 multiformats@9.9.0: 915 1123 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 916 1124 1125 + nanoid@3.3.11: 1126 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 1127 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1128 + hasBin: true 1129 + 1130 + obug@2.1.1: 1131 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 1132 + 917 1133 package-json-from-dist@1.0.1: 918 1134 resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 919 1135 ··· 928 1144 resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} 929 1145 engines: {node: 20 || >=22} 930 1146 1147 + pathe@2.0.3: 1148 + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 1149 + 1150 + picocolors@1.1.1: 1151 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1152 + 931 1153 picomatch@2.3.1: 932 1154 resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 933 1155 engines: {node: '>=8.6'} 1156 + 1157 + picomatch@4.0.3: 1158 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 1159 + engines: {node: '>=12'} 1160 + 1161 + postcss@8.5.6: 1162 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1163 + engines: {node: ^10 || ^12 || >=14} 934 1164 935 1165 postgres@3.4.8: 936 1166 resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} ··· 951 1181 resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 952 1182 engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 953 1183 1184 + rollup@4.57.1: 1185 + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} 1186 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1187 + hasBin: true 1188 + 954 1189 run-parallel@1.2.0: 955 1190 resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 956 1191 ··· 962 1197 resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 963 1198 engines: {node: '>=8'} 964 1199 1200 + siginfo@2.0.0: 1201 + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1202 + 965 1203 signal-exit@4.1.0: 966 1204 resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 967 1205 engines: {node: '>=14'} 968 1206 1207 + source-map-js@1.2.1: 1208 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1209 + engines: {node: '>=0.10.0'} 1210 + 969 1211 source-map-support@0.5.21: 970 1212 resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 971 1213 ··· 973 1215 resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 974 1216 engines: {node: '>=0.10.0'} 975 1217 1218 + stackback@0.0.2: 1219 + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1220 + 1221 + std-env@3.10.0: 1222 + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 1223 + 976 1224 supports-color@7.2.0: 977 1225 resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 978 1226 engines: {node: '>=8'} 1227 + 1228 + tinybench@2.9.0: 1229 + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1230 + 1231 + tinyexec@1.0.2: 1232 + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} 1233 + engines: {node: '>=18'} 1234 + 1235 + tinyglobby@0.2.15: 1236 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 1237 + engines: {node: '>=12.0.0'} 1238 + 1239 + tinyrainbow@3.0.3: 1240 + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} 1241 + engines: {node: '>=14.0.0'} 979 1242 980 1243 tlds@1.261.0: 981 1244 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} ··· 1052 1315 unicode-segmenter@0.14.5: 1053 1316 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 1054 1317 1318 + vite@7.3.1: 1319 + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} 1320 + engines: {node: ^20.19.0 || >=22.12.0} 1321 + hasBin: true 1322 + peerDependencies: 1323 + '@types/node': ^20.19.0 || >=22.12.0 1324 + jiti: '>=1.21.0' 1325 + less: ^4.0.0 1326 + lightningcss: ^1.21.0 1327 + sass: ^1.70.0 1328 + sass-embedded: ^1.70.0 1329 + stylus: '>=0.54.8' 1330 + sugarss: ^5.0.0 1331 + terser: ^5.16.0 1332 + tsx: ^4.8.1 1333 + yaml: ^2.4.2 1334 + peerDependenciesMeta: 1335 + '@types/node': 1336 + optional: true 1337 + jiti: 1338 + optional: true 1339 + less: 1340 + optional: true 1341 + lightningcss: 1342 + optional: true 1343 + sass: 1344 + optional: true 1345 + sass-embedded: 1346 + optional: true 1347 + stylus: 1348 + optional: true 1349 + sugarss: 1350 + optional: true 1351 + terser: 1352 + optional: true 1353 + tsx: 1354 + optional: true 1355 + yaml: 1356 + optional: true 1357 + 1358 + vitest@4.0.18: 1359 + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} 1360 + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} 1361 + hasBin: true 1362 + peerDependencies: 1363 + '@edge-runtime/vm': '*' 1364 + '@opentelemetry/api': ^1.9.0 1365 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 1366 + '@vitest/browser-playwright': 4.0.18 1367 + '@vitest/browser-preview': 4.0.18 1368 + '@vitest/browser-webdriverio': 4.0.18 1369 + '@vitest/ui': 4.0.18 1370 + happy-dom: '*' 1371 + jsdom: '*' 1372 + peerDependenciesMeta: 1373 + '@edge-runtime/vm': 1374 + optional: true 1375 + '@opentelemetry/api': 1376 + optional: true 1377 + '@types/node': 1378 + optional: true 1379 + '@vitest/browser-playwright': 1380 + optional: true 1381 + '@vitest/browser-preview': 1382 + optional: true 1383 + '@vitest/browser-webdriverio': 1384 + optional: true 1385 + '@vitest/ui': 1386 + optional: true 1387 + happy-dom: 1388 + optional: true 1389 + jsdom: 1390 + optional: true 1391 + 1055 1392 which@2.0.2: 1056 1393 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1057 1394 engines: {node: '>= 8'} 1395 + hasBin: true 1396 + 1397 + why-is-node-running@2.3.0: 1398 + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1399 + engines: {node: '>=8'} 1058 1400 hasBin: true 1059 1401 1060 1402 yaml@2.8.2: ··· 1384 1726 1385 1727 '@isaacs/cliui@9.0.0': {} 1386 1728 1729 + '@jridgewell/sourcemap-codec@1.5.5': {} 1730 + 1387 1731 '@nodelib/fs.scandir@2.1.5': 1388 1732 dependencies: 1389 1733 '@nodelib/fs.stat': 2.0.5 ··· 1396 1740 '@nodelib/fs.scandir': 2.1.5 1397 1741 fastq: 1.20.1 1398 1742 1743 + '@rollup/rollup-android-arm-eabi@4.57.1': 1744 + optional: true 1745 + 1746 + '@rollup/rollup-android-arm64@4.57.1': 1747 + optional: true 1748 + 1749 + '@rollup/rollup-darwin-arm64@4.57.1': 1750 + optional: true 1751 + 1752 + '@rollup/rollup-darwin-x64@4.57.1': 1753 + optional: true 1754 + 1755 + '@rollup/rollup-freebsd-arm64@4.57.1': 1756 + optional: true 1757 + 1758 + '@rollup/rollup-freebsd-x64@4.57.1': 1759 + optional: true 1760 + 1761 + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': 1762 + optional: true 1763 + 1764 + '@rollup/rollup-linux-arm-musleabihf@4.57.1': 1765 + optional: true 1766 + 1767 + '@rollup/rollup-linux-arm64-gnu@4.57.1': 1768 + optional: true 1769 + 1770 + '@rollup/rollup-linux-arm64-musl@4.57.1': 1771 + optional: true 1772 + 1773 + '@rollup/rollup-linux-loong64-gnu@4.57.1': 1774 + optional: true 1775 + 1776 + '@rollup/rollup-linux-loong64-musl@4.57.1': 1777 + optional: true 1778 + 1779 + '@rollup/rollup-linux-ppc64-gnu@4.57.1': 1780 + optional: true 1781 + 1782 + '@rollup/rollup-linux-ppc64-musl@4.57.1': 1783 + optional: true 1784 + 1785 + '@rollup/rollup-linux-riscv64-gnu@4.57.1': 1786 + optional: true 1787 + 1788 + '@rollup/rollup-linux-riscv64-musl@4.57.1': 1789 + optional: true 1790 + 1791 + '@rollup/rollup-linux-s390x-gnu@4.57.1': 1792 + optional: true 1793 + 1794 + '@rollup/rollup-linux-x64-gnu@4.57.1': 1795 + optional: true 1796 + 1797 + '@rollup/rollup-linux-x64-musl@4.57.1': 1798 + optional: true 1799 + 1800 + '@rollup/rollup-openbsd-x64@4.57.1': 1801 + optional: true 1802 + 1803 + '@rollup/rollup-openharmony-arm64@4.57.1': 1804 + optional: true 1805 + 1806 + '@rollup/rollup-win32-arm64-msvc@4.57.1': 1807 + optional: true 1808 + 1809 + '@rollup/rollup-win32-ia32-msvc@4.57.1': 1810 + optional: true 1811 + 1812 + '@rollup/rollup-win32-x64-gnu@4.57.1': 1813 + optional: true 1814 + 1815 + '@rollup/rollup-win32-x64-msvc@4.57.1': 1816 + optional: true 1817 + 1818 + '@standard-schema/spec@1.1.0': {} 1819 + 1399 1820 '@ts-morph/common@0.17.0': 1400 1821 dependencies: 1401 1822 fast-glob: 3.3.3 1402 1823 minimatch: 5.1.6 1403 1824 mkdirp: 1.0.4 1404 1825 path-browserify: 1.0.1 1826 + 1827 + '@types/chai@5.2.3': 1828 + dependencies: 1829 + '@types/deep-eql': 4.0.2 1830 + assertion-error: 2.0.1 1831 + 1832 + '@types/deep-eql@4.0.2': {} 1833 + 1834 + '@types/estree@1.0.8': {} 1405 1835 1406 1836 '@types/node@22.19.9': 1407 1837 dependencies: 1408 1838 undici-types: 6.21.0 1409 1839 1840 + '@vitest/expect@4.0.18': 1841 + dependencies: 1842 + '@standard-schema/spec': 1.1.0 1843 + '@types/chai': 5.2.3 1844 + '@vitest/spy': 4.0.18 1845 + '@vitest/utils': 4.0.18 1846 + chai: 6.2.2 1847 + tinyrainbow: 3.0.3 1848 + 1849 + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2))': 1850 + dependencies: 1851 + '@vitest/spy': 4.0.18 1852 + estree-walker: 3.0.3 1853 + magic-string: 0.30.21 1854 + optionalDependencies: 1855 + vite: 7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 1856 + 1857 + '@vitest/pretty-format@4.0.18': 1858 + dependencies: 1859 + tinyrainbow: 3.0.3 1860 + 1861 + '@vitest/runner@4.0.18': 1862 + dependencies: 1863 + '@vitest/utils': 4.0.18 1864 + pathe: 2.0.3 1865 + 1866 + '@vitest/snapshot@4.0.18': 1867 + dependencies: 1868 + '@vitest/pretty-format': 4.0.18 1869 + magic-string: 0.30.21 1870 + pathe: 2.0.3 1871 + 1872 + '@vitest/spy@4.0.18': {} 1873 + 1874 + '@vitest/utils@4.0.18': 1875 + dependencies: 1876 + '@vitest/pretty-format': 4.0.18 1877 + tinyrainbow: 3.0.3 1878 + 1410 1879 ansi-styles@4.3.0: 1411 1880 dependencies: 1412 1881 color-convert: 2.0.1 1882 + 1883 + assertion-error@2.0.1: {} 1413 1884 1414 1885 await-lock@2.2.2: {} 1415 1886 ··· 1424 1895 fill-range: 7.1.1 1425 1896 1426 1897 buffer-from@1.1.2: {} 1898 + 1899 + chai@6.2.2: {} 1427 1900 1428 1901 chalk@4.1.2: 1429 1902 dependencies: ··· 1462 1935 drizzle-orm@0.45.1(postgres@3.4.8): 1463 1936 optionalDependencies: 1464 1937 postgres: 3.4.8 1938 + 1939 + es-module-lexer@1.7.0: {} 1465 1940 1466 1941 esbuild-register@3.6.0(esbuild@0.25.12): 1467 1942 dependencies: ··· 1553 2028 '@esbuild/win32-ia32': 0.27.3 1554 2029 '@esbuild/win32-x64': 0.27.3 1555 2030 2031 + estree-walker@3.0.3: 2032 + dependencies: 2033 + '@types/estree': 1.0.8 2034 + 2035 + expect-type@1.3.0: {} 2036 + 1556 2037 fast-glob@3.3.3: 1557 2038 dependencies: 1558 2039 '@nodelib/fs.stat': 2.0.5 ··· 1564 2045 fastq@1.20.1: 1565 2046 dependencies: 1566 2047 reusify: 1.1.0 2048 + 2049 + fdir@6.5.0(picomatch@4.0.3): 2050 + optionalDependencies: 2051 + picomatch: 4.0.3 1567 2052 1568 2053 fill-range@7.1.1: 1569 2054 dependencies: ··· 1616 2101 1617 2102 lru-cache@11.2.5: {} 1618 2103 2104 + magic-string@0.30.21: 2105 + dependencies: 2106 + '@jridgewell/sourcemap-codec': 1.5.5 2107 + 1619 2108 merge2@1.4.1: {} 1620 2109 1621 2110 micromatch@4.0.8: ··· 1639 2128 1640 2129 multiformats@9.9.0: {} 1641 2130 2131 + nanoid@3.3.11: {} 2132 + 2133 + obug@2.1.1: {} 2134 + 1642 2135 package-json-from-dist@1.0.1: {} 1643 2136 1644 2137 path-browserify@1.0.1: {} ··· 1649 2142 dependencies: 1650 2143 lru-cache: 11.2.5 1651 2144 minipass: 7.1.2 2145 + 2146 + pathe@2.0.3: {} 2147 + 2148 + picocolors@1.1.1: {} 1652 2149 1653 2150 picomatch@2.3.1: {} 1654 2151 2152 + picomatch@4.0.3: {} 2153 + 2154 + postcss@8.5.6: 2155 + dependencies: 2156 + nanoid: 3.3.11 2157 + picocolors: 1.1.1 2158 + source-map-js: 1.2.1 2159 + 1655 2160 postgres@3.4.8: {} 1656 2161 1657 2162 prettier@3.8.1: {} ··· 1662 2167 1663 2168 reusify@1.1.0: {} 1664 2169 2170 + rollup@4.57.1: 2171 + dependencies: 2172 + '@types/estree': 1.0.8 2173 + optionalDependencies: 2174 + '@rollup/rollup-android-arm-eabi': 4.57.1 2175 + '@rollup/rollup-android-arm64': 4.57.1 2176 + '@rollup/rollup-darwin-arm64': 4.57.1 2177 + '@rollup/rollup-darwin-x64': 4.57.1 2178 + '@rollup/rollup-freebsd-arm64': 4.57.1 2179 + '@rollup/rollup-freebsd-x64': 4.57.1 2180 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 2181 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 2182 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 2183 + '@rollup/rollup-linux-arm64-musl': 4.57.1 2184 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 2185 + '@rollup/rollup-linux-loong64-musl': 4.57.1 2186 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 2187 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 2188 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 2189 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 2190 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 2191 + '@rollup/rollup-linux-x64-gnu': 4.57.1 2192 + '@rollup/rollup-linux-x64-musl': 4.57.1 2193 + '@rollup/rollup-openbsd-x64': 4.57.1 2194 + '@rollup/rollup-openharmony-arm64': 4.57.1 2195 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 2196 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 2197 + '@rollup/rollup-win32-x64-gnu': 4.57.1 2198 + '@rollup/rollup-win32-x64-msvc': 4.57.1 2199 + fsevents: 2.3.3 2200 + 1665 2201 run-parallel@1.2.0: 1666 2202 dependencies: 1667 2203 queue-microtask: 1.2.3 ··· 1672 2208 1673 2209 shebang-regex@3.0.0: {} 1674 2210 2211 + siginfo@2.0.0: {} 2212 + 1675 2213 signal-exit@4.1.0: {} 2214 + 2215 + source-map-js@1.2.1: {} 1676 2216 1677 2217 source-map-support@0.5.21: 1678 2218 dependencies: ··· 1681 2221 1682 2222 source-map@0.6.1: {} 1683 2223 2224 + stackback@0.0.2: {} 2225 + 2226 + std-env@3.10.0: {} 2227 + 1684 2228 supports-color@7.2.0: 1685 2229 dependencies: 1686 2230 has-flag: 4.0.0 2231 + 2232 + tinybench@2.9.0: {} 2233 + 2234 + tinyexec@1.0.2: {} 2235 + 2236 + tinyglobby@0.2.15: 2237 + dependencies: 2238 + fdir: 6.5.0(picomatch@4.0.3) 2239 + picomatch: 4.0.3 2240 + 2241 + tinyrainbow@3.0.3: {} 1687 2242 1688 2243 tlds@1.261.0: {} 1689 2244 ··· 1748 2303 1749 2304 unicode-segmenter@0.14.5: {} 1750 2305 2306 + vite@7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2): 2307 + dependencies: 2308 + esbuild: 0.27.3 2309 + fdir: 6.5.0(picomatch@4.0.3) 2310 + picomatch: 4.0.3 2311 + postcss: 8.5.6 2312 + rollup: 4.57.1 2313 + tinyglobby: 0.2.15 2314 + optionalDependencies: 2315 + '@types/node': 22.19.9 2316 + fsevents: 2.3.3 2317 + tsx: 4.21.0 2318 + yaml: 2.8.2 2319 + 2320 + vitest@4.0.18(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2): 2321 + dependencies: 2322 + '@vitest/expect': 4.0.18 2323 + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2)) 2324 + '@vitest/pretty-format': 4.0.18 2325 + '@vitest/runner': 4.0.18 2326 + '@vitest/snapshot': 4.0.18 2327 + '@vitest/spy': 4.0.18 2328 + '@vitest/utils': 4.0.18 2329 + es-module-lexer: 1.7.0 2330 + expect-type: 1.3.0 2331 + magic-string: 0.30.21 2332 + obug: 2.1.1 2333 + pathe: 2.0.3 2334 + picomatch: 4.0.3 2335 + std-env: 3.10.0 2336 + tinybench: 2.9.0 2337 + tinyexec: 1.0.2 2338 + tinyglobby: 0.2.15 2339 + tinyrainbow: 3.0.3 2340 + vite: 7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 2341 + why-is-node-running: 2.3.0 2342 + optionalDependencies: 2343 + '@types/node': 22.19.9 2344 + transitivePeerDependencies: 2345 + - jiti 2346 + - less 2347 + - lightningcss 2348 + - msw 2349 + - sass 2350 + - sass-embedded 2351 + - stylus 2352 + - sugarss 2353 + - terser 2354 + - tsx 2355 + - yaml 2356 + 1751 2357 which@2.0.2: 1752 2358 dependencies: 1753 2359 isexe: 2.0.0 2360 + 2361 + why-is-node-running@2.3.0: 2362 + dependencies: 2363 + siginfo: 2.0.0 2364 + stackback: 0.0.2 1754 2365 1755 2366 yaml@2.8.2: {} 1756 2367
+3
turbo.json
··· 14 14 "lint": { 15 15 "dependsOn": ["^build"] 16 16 }, 17 + "test": { 18 + "dependsOn": ["^build"] 19 + }, 17 20 "clean": { 18 21 "cache": false 19 22 }
+8
vitest.workspace.ts
··· 1 + import { defineWorkspace } from "vitest/config"; 2 + 3 + export default defineWorkspace([ 4 + "apps/appview", 5 + "apps/web", 6 + "packages/db", 7 + "packages/lexicon", 8 + ]);