diff --git a/client/src/lib/index.test.js b/client/src/lib/index.test.js index e49752b42a..df10d06cb9 100644 --- a/client/src/lib/index.test.js +++ b/client/src/lib/index.test.js @@ -9,20 +9,22 @@ const BARREL_SRC = readFileSync(join(HERE, 'index.js'), 'utf8'); const README_SRC = readFileSync(join(HERE, 'README.md'), 'utf8'); const sourceFiles = readdirSync(HERE).filter( - (f) => f.endsWith('.js') && !f.endsWith('.test.js') && f !== 'index.js', + (f) => (f.endsWith('.js') || f.endsWith('.jsx')) + && !f.endsWith('.test.js') && !f.endsWith('.test.jsx') + && f !== 'index.js', ); describe('client/src/lib/ barrel', () => { - it('re-exports every non-test .js file from index.js', () => { + it('re-exports every non-test source file from index.js', () => { expect(Object.keys(barrel).length).toBeGreaterThan(0); for (const f of sourceFiles) { expect(BARREL_SRC, `missing barrel re-export for ${f}`).toContain(`'./${f}'`); } }); - it('every non-test .js file has a README row', () => { + it('every non-test source file has a README row', () => { for (const f of sourceFiles) { - const base = f.replace(/\.js$/, ''); + const base = f.replace(/\.jsx?$/, ''); expect(README_SRC, `missing README entry for ${f}`).toContain(base); } }); diff --git a/server/lib/README.md b/server/lib/README.md index 885a9a2182..1b300e2645 100644 --- a/server/lib/README.md +++ b/server/lib/README.md @@ -13,7 +13,7 @@ grep -i "what you want to do" server/lib/README.md ``` The barrel `server/lib/index.js` is a machine-checkable enumeration of every public surface; -`server/lib/index.test.js` verifies that every non-test `.js` file is re-exported AND appears in this README, AND that no two flat-exported modules share an identifier name. +`server/lib/index.test.js` verifies that every non-test `.js`/`.jsx` file is re-exported AND appears in this README, AND that no two flat-exported modules share an identifier name. **Namespace exports.** The validation modules (`brainValidation`, `digitalTwinValidation`, etc.), `runners`, and `storyBible` are surfaced through the barrel as namespace exports — `barrel.brainValidation.settingsUpdateInputSchema`, not bare `settingsUpdateInputSchema` — because their generic names collide with peers. Direct deep imports (`import { settingsUpdateInputSchema } from './brainValidation.js'`) are unaffected. @@ -414,6 +414,7 @@ The barrel `server/lib/index.js` is a machine-checkable enumeration of every pub | `cudaCapability.js` | NVIDIA CUDA host probe — the one place that shells to `nvidia-smi`. `detectCudaGpus()` / cached `getCudaCapability()` return `{ available, status, gpus, maxVramGb }` where `status` is three-way: `'available'`, `'absent'` (no driver, or driver present with zero GPUs), or `'unknown'` (nvidia-smi exists but wouldn't answer — `available` is `null`, never `false`, so callers can say "couldn't detect" instead of lying about the hardware). `unknown` is not memoized so a transient driver hiccup retries. `parseNvidiaSmiGpus(stdout)` is the pure CSV parser (a `[N/A]` VRAM column yields `vramGb: null`, not a dropped GPU). Gates the image-to-3D `local-cuda` lane and the Windows FLUX.2 CUDA-torch wheel choice (`pythonSetup.js#hasNvidiaGpu`). `detectCudaComputeCapability()` / cached `getCudaComputeCapability()` are a SEPARATE `nvidia-smi` query (`--query-gpu=name,compute_cap,memory.total`, parsed by `parseNvidiaSmiComputeCaps`) returning `primaryComputeCap` — the arch of the LARGEST card, since a render uses one GPU — for build flags like NATTEN's `NATTEN_CUDA_ARCH` and SGLang provider filtering; kept separate because an older driver rejects `compute_cap` and would fail the whole VRAM query with it. `detectCudaUtilization()` / `getCudaUtilization()` add live per-GPU utilization on a short TTL. | | `systemCapabilities.js` | Machine-local capability snapshot plus the shared three-state hardware requirement evaluator. Captures coarse platform, architecture, Apple Silicon, memory, CPU, and cached CUDA facts; derives requirements for media models, local-LLM catalog entries, and provider runtimes; hides only known-incompatible choices while preserving `unknown` probe results. | | `db.js` | PostgreSQL connection pool. | +| `db/` | Boot DDL for the PostgreSQL schema, split per domain (#2832). `db/schema/index.js` re-exports each module's statement array and composes the two ordered lists `ensureSchemaImpl()` runs on every boot (`buildUpgradeDdl()` then `buildCatalogDdl()`). **Statement order is load-bearing** — append inside the domain module and leave the composer order alone. See `db/schema/README.md` for the module catalog. | | `pgTimestamp.js` | `mirrorTimestamp(value, fallback)` — coerce a hand-editable timestamp into a value Postgres TIMESTAMPTZ always accepts (or fall back), guarding boot-time binds against `Date.parse` rollover + out-of-range years. | | `pgTools.js` | `pg_dump` binary resolution shared by the backup snapshot path and the native↔Docker export path: `resolvePgDumpBinary(serverMajor)` (PORTOS_PGDUMP override → version-aware auto-select → bare `pg_dump`), plus the lower-level `pickPgDump` / `discoverPgDumpCandidates` / `resolvePgDump`. Picks the closest installed `pg_dump` whose major is ≥ the running server's. | | `ports.js` | Canonical PORTS object (re-exported from `ecosystem.config.cjs`). | diff --git a/server/lib/db/schema/index.test.js b/server/lib/db/schema/index.test.js new file mode 100644 index 0000000000..0f5b1abd64 --- /dev/null +++ b/server/lib/db/schema/index.test.js @@ -0,0 +1,101 @@ +/** + * Directory-driven guard for the schema composer (#5682). + * + * schema.test.js pins the composer against a HAND-WRITTEN list of module + * names, so it can only check the modules someone remembered to add to it. + * This file derives its expectations from `readdirSync` instead, which is the + * half that catches the real failure mode: adding + * `server/lib/db/schema/foo.js` and wiring only the `import` + `export {}` + * block ships a module whose `CREATE TABLE` never runs, so a fresh install + * (and every peer install upgrading) is missing the table and the feature + * fails at first query with a Postgres `relation does not exist`. + * + * The directory-level barrel/README guard in server/lib/index.test.js does not + * reach here — its `readdirSync` is non-recursive — hence this local copy of + * the same contract, modeled on server/lib/editorial/checkInfraBarrel.test.js. + * + * Out of scope: DDL statement text and ordering (schema.test.js and + * db.catalogDdlParity.test.js own those). + */ + +import { describe, it, expect } from 'vitest'; +import { readdirSync, readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { buildUpgradeDdl, buildCatalogDdl } from './index.js'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const BARREL_SRC = readFileSync(join(HERE, 'index.js'), 'utf8'); +const README_SRC = readFileSync(join(HERE, 'README.md'), 'utf8'); + +const MODULE_FILES = readdirSync(HERE) + .filter((f) => f.endsWith('.js') && !f.endsWith('.test.js') && f !== 'index.js') + .sort(); + +// Membership is checked statement-by-statement rather than by array identity, +// because a module legitimately contributes more than one separately-positioned +// array (catalog.js → `catalogDdl` + `catalogUserTypesDdl`) and audit.js +// contributes `auditDdl` plus the generated `buildAuditTriggers()`. Checking +// EVERY statement — not just the array's first — also catches a composer that +// spreads only part of an array. Membership encodes no ordering: statement +// order stays schema.test.js's job. +const COMPOSED_STATEMENTS = new Set([...buildUpgradeDdl(), ...buildCatalogDdl()]); + +const isBuilderName = (name) => /^build.*(Ddl|Triggers)$/.test(name); + +describe('db/schema composer covers every module in the directory (#5682)', () => { + it('found the domain modules', () => { + expect(MODULE_FILES.length).toBeGreaterThan(0); + }); + + it('index.js imports every non-test module', () => { + for (const f of MODULE_FILES) { + expect(BARREL_SRC, `db/schema/${f} is never imported by index.js`).toContain(`'./${f}'`); + } + }); + + it.each(MODULE_FILES)('%s contributes DDL and every statement of it is composed', async (f) => { + const mod = await import(/* @vite-ignore */ `./${f}`); + + // Two shapes contribute DDL here, and both must be checked: a plain + // statement array (`Ddl`) and a zero-arg generator that derives + // statements from a table list (audit.js's `buildAuditTriggers()`). + // Checking only the arrays would let a module's generated statements be + // dropped from the composer unnoticed. + const contributions = Object.entries(mod).flatMap(([name, value]) => { + if (name.endsWith('Ddl') && !isBuilderName(name)) return [[name, value]]; + if (isBuilderName(name) && typeof value === 'function' && value.length === 0) { + return [[`${name}()`, value()]]; + } + return []; + }); + + // Naming convention is the hook this guard hangs on: a module whose DDL + // export matches neither shape would silently opt out of the check below. + expect( + contributions.length, + `${f} exports no *Ddl array and no build*Ddl/build*Triggers() generator (see README.md)`, + ).toBeGreaterThan(0); + + for (const [name, statements] of contributions) { + expect(Array.isArray(statements), `${f} export '${name}' is not a statement array`).toBe(true); + expect(statements.length, `${f} export '${name}' is empty`).toBeGreaterThan(0); + const uncomposed = statements.filter((sql) => !COMPOSED_STATEMENTS.has(sql)); + expect( + uncomposed.length, + `${f} export '${name}' has ${uncomposed.length} statement(s) that never run — spread it into ` + + `buildUpgradeDdl() or buildCatalogDdl() in index.js. First: ${uncomposed[0]?.slice(0, 120)}`, + ).toBe(0); + } + }); + + it('every non-test module has a backtick-wrapped README row', () => { + // Require the documented-row form (`module.js`) rather than a bare + // substring, so a name appearing only in prose can't satisfy the guard. + for (const f of MODULE_FILES) { + expect(README_SRC, `missing README row for db/schema/${f}`).toContain('`' + f + '`'); + } + }); +}); +// @vitest-environment node diff --git a/server/lib/index.test.js b/server/lib/index.test.js index a88cbf97f4..9e2ec6ebc8 100644 --- a/server/lib/index.test.js +++ b/server/lib/index.test.js @@ -9,7 +9,9 @@ const BARREL_SRC = readFileSync(join(HERE, 'index.js'), 'utf8'); const README_SRC = readFileSync(join(HERE, 'README.md'), 'utf8'); const sourceFiles = readdirSync(HERE) - .filter((f) => f.endsWith('.js') && !f.endsWith('.test.js') && f !== 'index.js'); + .filter((f) => (f.endsWith('.js') || f.endsWith('.jsx')) + && !f.endsWith('.test.js') && !f.endsWith('.test.jsx') + && f !== 'index.js'); // The barrel is the machine-checkable enumeration of every public surface in // `server/lib/`. If a `export * from './foo.js'` line points to a non-existent @@ -18,7 +20,7 @@ const sourceFiles = readdirSync(HERE) // a README row. Both halves keep the discovery contract in AGENTS.md honest. describe('server/lib/ barrel', () => { - it('re-exports every non-test .js file from index.js', () => { + it('re-exports every non-test source file from index.js', () => { // The `import * as barrel` above is the load: a star-export of a missing // module throws before this body runs. The length check keeps `barrel` live // so an unused-import lint can't delete that load. @@ -30,12 +32,12 @@ describe('server/lib/ barrel', () => { } }); - it('every non-test .js file has a README row', () => { + it('every non-test source file has a README row', () => { // Forces the catalog parity: a new helper must also get a one-line // README entry. Looser match (filename anywhere in the README) so the // table format can evolve without breaking this guard. for (const f of sourceFiles) { - const base = f.replace(/\.js$/, ''); + const base = f.replace(/\.jsx?$/, ''); expect(README_SRC, `missing README entry for ${f}`).toContain(base); } });