From 3ec6a3a546a7c9665ead08c0052e19ab3b4ec8ba Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 3 Aug 2026 16:03:29 -0500 Subject: [PATCH 1/5] fix(db): tolerate a SQLite build without the dbstat virtual table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getDatabaseStats() queried `dbstat` once per table with no guard. `dbstat` is compile-time optional (ENABLE_DBSTAT_VTAB) and is absent from sql.js/WASM builds, so on those runtimes the query throws and the error propagates out of getDatabaseStats(). Every caller dies with it. Most visibly, GET and PATCH /api/settings/database return HTTP 500, which makes the entire database settings page unusable — users cannot read or change page size, cache size, or vacuum settings. The function already anticipated missing virtual-table modules: the COUNT(*) lookup a few lines above swallows "no such module:" errors. The dbstat query simply sat outside that guard. Probe dbstat once per call and skip the per-table size lookups when it is unavailable, reporting size 0. Database-level figures (total size, page count, cache size) come from pragmas and stay accurate; only per-table byte sizes are lost, which is the correct trade against a hard 500. Unrelated failures (I/O errors, corruption) still propagate. Both spellings are handled: sql.js reports "no such module: dbstat" while better-sqlite3 can surface "no such table: dbstat". --- src/lib/db/stats.ts | 37 +++++- tests/unit/db/stats-dbstat-optional.test.ts | 127 ++++++++++++++++++++ 2 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 tests/unit/db/stats-dbstat-optional.test.ts diff --git a/src/lib/db/stats.ts b/src/lib/db/stats.ts index e1a824ad90e..cf5d4fb36a3 100644 --- a/src/lib/db/stats.ts +++ b/src/lib/db/stats.ts @@ -24,6 +24,26 @@ export interface DatabaseStats { cacheSize: number; } +/** + * `dbstat` is a compile-time-optional SQLite virtual table (ENABLE_DBSTAT_VTAB). + * It is unavailable on sql.js/WASM and on any build compiled without it, where + * querying it throws "no such module: dbstat" (some drivers report "no such + * table: dbstat"). Per-table byte sizes are a nice-to-have, so probe once and + * degrade to 0 rather than failing the whole stats call — and with it every + * caller, including the database settings API. + */ +function isDbstatAvailable(db: SqliteAdapter): boolean { + try { + db.prepare(`SELECT SUM(pgsize) as size FROM dbstat WHERE name = ?`).get("sqlite_master"); + return true; + } catch (error) { + if (error instanceof Error && /no such (module|table): dbstat/i.test(error.message)) { + return false; + } + throw error; + } +} + export function getDatabaseStats(db: SqliteAdapter = getDbInstance()): DatabaseStats { const pageSize = db.pragma("page_size", { simple: true }) as number; const pageCount = db.pragma("page_count", { simple: true }) as number; @@ -36,12 +56,15 @@ export function getDatabaseStats(db: SqliteAdapter = getDbInstance()): DatabaseS ) .all() as Array<{ name: string }>; + const dbstatAvailable = isDbstatAvailable(db); + const tableStats = tables.map((table) => { let rowCount = 0; try { const quotedName = `"${table.name.replaceAll('"', '""')}"`; const row = db.prepare(`SELECT COUNT(*) as count FROM ${quotedName}`).get() as - { count: number } | undefined; + | { count: number } + | undefined; rowCount = row?.count ?? 0; } catch (error) { if (!(error instanceof Error) || !error.message.startsWith("no such module:")) { @@ -50,14 +73,18 @@ export function getDatabaseStats(db: SqliteAdapter = getDbInstance()): DatabaseS // Optional virtual-table modules may be unavailable on this connection. } - const tableSize = db - .prepare(`SELECT SUM(pgsize) as size FROM dbstat WHERE name = ?`) - .get(table.name) as { size: number | null }; + let size = 0; + if (dbstatAvailable) { + const tableSize = db + .prepare(`SELECT SUM(pgsize) as size FROM dbstat WHERE name = ?`) + .get(table.name) as { size: number | null } | undefined; + size = tableSize?.size || 0; + } return { name: table.name, rowCount, - size: tableSize?.size || 0, + size, }; }); diff --git a/tests/unit/db/stats-dbstat-optional.test.ts b/tests/unit/db/stats-dbstat-optional.test.ts new file mode 100644 index 00000000000..6c99b382139 --- /dev/null +++ b/tests/unit/db/stats-dbstat-optional.test.ts @@ -0,0 +1,127 @@ +/** + * getDatabaseStats() must survive a SQLite build without the `dbstat` virtual + * table. + * + * `dbstat` is compile-time optional (ENABLE_DBSTAT_VTAB) and is absent from + * sql.js/WASM builds. Before the fix, the unguarded per-table `SELECT SUM(pgsize) + * FROM dbstat` threw, which propagated out of getDatabaseStats() and made + * GET/PATCH /api/settings/database return HTTP 500 — the whole database settings + * page became unusable on those runtimes. + */ + +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { getDatabaseStats } from "@/lib/db/stats"; +import type { PreparedStatement, SqliteAdapter } from "@/lib/db/adapters/types"; + +type FakeOptions = { + /** Error message thrown by any statement touching `dbstat`. */ + dbstatError?: string; +}; + +/** + * Minimal in-memory SqliteAdapter double. Only the surface getDatabaseStats() + * actually touches is implemented; everything else throws so an accidental new + * dependency shows up loudly instead of silently passing. + */ +function createFakeDb({ dbstatError }: FakeOptions = {}): SqliteAdapter { + const tables = ["alpha", "beta"]; + + const prepare = (sql: string): PreparedStatement => { + const touchesDbstat = /\bdbstat\b/i.test(sql); + + return { + run() { + throw new Error(`unexpected run(): ${sql}`); + }, + get(...params: unknown[]) { + if (touchesDbstat) { + if (dbstatError) throw new Error(dbstatError); + // 4 KiB per table when dbstat is available. + return { size: params[0] === "sqlite_master" ? 0 : 4096 }; + } + if (/COUNT\(\*\)/i.test(sql)) return { count: 7 }; + throw new Error(`unexpected get(): ${sql}`); + }, + all() { + if (/type='table'/i.test(sql)) return tables.map((name) => ({ name })); + if (/type='index'/i.test(sql)) return [{ name: "idx_alpha", tableName: "alpha" }]; + throw new Error(`unexpected all(): ${sql}`); + }, + }; + }; + + return { + driver: "sql.js", + open: true, + name: ":memory:", + prepare, + exec() {}, + pragma(pragmaStr: string) { + if (pragmaStr === "page_size") return 4096; + if (pragmaStr === "page_count") return 100; + if (pragmaStr === "cache_size") return -65536; + throw new Error(`unexpected pragma: ${pragmaStr}`); + }, + transaction(fn: (...args: unknown[]) => T) { + return fn; + }, + immediate(fn: () => void) { + fn(); + }, + async backup() {}, + checkpoint() {}, + close() {}, + raw: null, + } satisfies SqliteAdapter; +} + +test("getDatabaseStats reports per-table sizes when dbstat is available", () => { + const stats = getDatabaseStats(createFakeDb()); + + assert.equal(stats.totalSize, 4096 * 100); + assert.deepEqual( + stats.tables.map((t) => [t.name, t.rowCount, t.size]), + [ + ["alpha", 7, 4096], + ["beta", 7, 4096], + ] + ); +}); + +test("getDatabaseStats degrades to size 0 when dbstat module is missing", () => { + const stats = getDatabaseStats(createFakeDb({ dbstatError: "no such module: dbstat" })); + + // The call must succeed; only per-table byte sizes are lost. + assert.deepEqual( + stats.tables.map((t) => [t.name, t.rowCount, t.size]), + [ + ["alpha", 7, 0], + ["beta", 7, 0], + ] + ); + // Database-level numbers come from pragmas and stay accurate. + assert.equal(stats.totalSize, 4096 * 100); + assert.equal(stats.pageCount, 100); + assert.equal(stats.cacheSize, -65536); + assert.equal(stats.indexes.length, 1); +}); + +test("getDatabaseStats degrades when the driver reports dbstat as a missing table", () => { + // better-sqlite3 surfaces this variant instead of "no such module". + const stats = getDatabaseStats(createFakeDb({ dbstatError: "no such table: dbstat" })); + + assert.deepEqual( + stats.tables.map((t) => t.size), + [0, 0] + ); +}); + +test("getDatabaseStats still propagates unrelated dbstat failures", () => { + // A genuine fault (disk I/O, corruption) must not be silently swallowed. + assert.throws( + () => getDatabaseStats(createFakeDb({ dbstatError: "database disk image is malformed" })), + /database disk image is malformed/ + ); +}); From dfa80e7e340a2cf6a4e72d19dacae1f75819240f Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 3 Aug 2026 16:35:47 -0500 Subject: [PATCH 2/5] test(db): cover prefixed driver errors and dbstat edge cases Review follow-up on the previous commit. The guard is deliberately unanchored because real drivers stringify errors with their class name attached ("SqliteError: no such table: dbstat", "RuntimeError: ..."). Nothing pinned that, so anchoring the regex would have passed the suite while silently breaking every real driver. Add a case for the prefixed form; it fails if a caret is introduced. Also cover three shapes the fake previously could not express: - a database with no user tables, which is what a fresh install hits first - SUM(pgsize) returning NULL for a table occupying no pages - dbstat answering the probe but failing on a later table, which documents that a mid-iteration fault still propagates rather than being mistaken for an absent module Correct the source comment: the two error spellings track the SQLite build, not the driver package, so the earlier attribution to better-sqlite3 was wrong. --- src/lib/db/stats.ts | 12 ++-- tests/unit/db/stats-dbstat-optional.test.ts | 78 ++++++++++++++++++--- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/lib/db/stats.ts b/src/lib/db/stats.ts index cf5d4fb36a3..e6402317943 100644 --- a/src/lib/db/stats.ts +++ b/src/lib/db/stats.ts @@ -26,11 +26,13 @@ export interface DatabaseStats { /** * `dbstat` is a compile-time-optional SQLite virtual table (ENABLE_DBSTAT_VTAB). - * It is unavailable on sql.js/WASM and on any build compiled without it, where - * querying it throws "no such module: dbstat" (some drivers report "no such - * table: dbstat"). Per-table byte sizes are a nice-to-have, so probe once and - * degrade to 0 rather than failing the whole stats call — and with it every - * caller, including the database settings API. + * Builds without it — sql.js/WASM among them — reject the query with either + * "no such module: dbstat" or "no such table: dbstat" depending on the build, + * and drivers prefix their error class onto the message, so match loosely. + * + * Per-table byte sizes are a nice-to-have, so probe once and degrade to 0 + * rather than failing the whole stats call — and with it every caller, + * including the database settings API. */ function isDbstatAvailable(db: SqliteAdapter): boolean { try { diff --git a/tests/unit/db/stats-dbstat-optional.test.ts b/tests/unit/db/stats-dbstat-optional.test.ts index 6c99b382139..7f92c1a0626 100644 --- a/tests/unit/db/stats-dbstat-optional.test.ts +++ b/tests/unit/db/stats-dbstat-optional.test.ts @@ -18,6 +18,12 @@ import type { PreparedStatement, SqliteAdapter } from "@/lib/db/adapters/types"; type FakeOptions = { /** Error message thrown by any statement touching `dbstat`. */ dbstatError?: string; + /** Tables reported by sqlite_master. */ + tables?: string[]; + /** Make the dbstat probe succeed but fail for this specific table. */ + failOnlyOn?: string; + /** Return `{ size: null }` from dbstat, as SUM() does over an empty table. */ + nullSize?: boolean; }; /** @@ -25,9 +31,12 @@ type FakeOptions = { * actually touches is implemented; everything else throws so an accidental new * dependency shows up loudly instead of silently passing. */ -function createFakeDb({ dbstatError }: FakeOptions = {}): SqliteAdapter { - const tables = ["alpha", "beta"]; - +function createFakeDb({ + dbstatError, + tables = ["alpha", "beta"], + failOnlyOn, + nullSize, +}: FakeOptions = {}): SqliteAdapter { const prepare = (sql: string): PreparedStatement => { const touchesDbstat = /\bdbstat\b/i.test(sql); @@ -37,16 +46,24 @@ function createFakeDb({ dbstatError }: FakeOptions = {}): SqliteAdapter { }, get(...params: unknown[]) { if (touchesDbstat) { - if (dbstatError) throw new Error(dbstatError); - // 4 KiB per table when dbstat is available. - return { size: params[0] === "sqlite_master" ? 0 : 4096 }; + const probing = params[0] === "sqlite_master"; + // `failOnlyOn` models a driver that answers the probe but fails later. + if (failOnlyOn) { + if (params[0] === failOnlyOn) throw new Error(dbstatError ?? "no such table: dbstat"); + } else if (dbstatError) { + throw new Error(dbstatError); + } + if (probing) return { size: 0 }; + return { size: nullSize ? null : 4096 }; } if (/COUNT\(\*\)/i.test(sql)) return { count: 7 }; throw new Error(`unexpected get(): ${sql}`); }, all() { if (/type='table'/i.test(sql)) return tables.map((name) => ({ name })); - if (/type='index'/i.test(sql)) return [{ name: "idx_alpha", tableName: "alpha" }]; + if (/type='index'/i.test(sql)) { + return tables.length ? [{ name: "idx_alpha", tableName: "alpha" }] : []; + } throw new Error(`unexpected all(): ${sql}`); }, }; @@ -109,7 +126,7 @@ test("getDatabaseStats degrades to size 0 when dbstat module is missing", () => }); test("getDatabaseStats degrades when the driver reports dbstat as a missing table", () => { - // better-sqlite3 surfaces this variant instead of "no such module". + // SQLite builds lacking ENABLE_DBSTAT_VTAB commonly report this variant. const stats = getDatabaseStats(createFakeDb({ dbstatError: "no such table: dbstat" })); assert.deepEqual( @@ -118,6 +135,51 @@ test("getDatabaseStats degrades when the driver reports dbstat as a missing tabl ); }); +test("getDatabaseStats degrades when the driver prefixes its error class", () => { + // Real drivers stringify as "SqliteError: ..." / "RuntimeError: ...", so the + // guard must not be anchored to the start of the message. + for (const message of [ + "SqliteError: no such table: dbstat", + "RuntimeError: no such module: dbstat", + ]) { + const stats = getDatabaseStats(createFakeDb({ dbstatError: message })); + assert.deepEqual( + stats.tables.map((t) => t.size), + [0, 0], + `expected degradation for ${message}` + ); + } +}); + +test("getDatabaseStats handles a database with no user tables", () => { + // The shape a fresh install hits before any migration has run. + const stats = getDatabaseStats(createFakeDb({ tables: [] })); + + assert.deepEqual(stats.tables, []); + assert.deepEqual(stats.indexes, []); + assert.equal(stats.totalSize, 4096 * 100); +}); + +test("getDatabaseStats maps a NULL dbstat sum to 0", () => { + // SUM(pgsize) returns NULL when a table occupies no pages. + const stats = getDatabaseStats(createFakeDb({ nullSize: true })); + + assert.deepEqual( + stats.tables.map((t) => t.size), + [0, 0] + ); +}); + +test("getDatabaseStats propagates a dbstat failure that appears after the probe", () => { + // Documents current behaviour: the probe establishes availability once, so a + // later per-table failure is treated as a genuine fault rather than a missing + // module. Anything else would mask real I/O errors mid-iteration. + assert.throws( + () => getDatabaseStats(createFakeDb({ failOnlyOn: "beta" })), + /no such table: dbstat/ + ); +}); + test("getDatabaseStats still propagates unrelated dbstat failures", () => { // A genuine fault (disk I/O, corruption) must not be silently swallowed. assert.throws( From 376173677a5cc904db3f8bd450f146a255e42a2a Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 3 Aug 2026 17:55:11 -0500 Subject: [PATCH 3/5] ci: restore the standalone artifact build for router deploys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit c317b6c82 moved this workflow to omniroute-ops, which builds a container image and is not yet the deployment path. The router still runs an unpacked standalone bundle under releases/, so without this workflow there is no way to produce a shippable artifact without building on the live host — which caused a ~30 minute fleet outage on 2026-07-26. Restored verbatim from 2fc1229fe, the commit that produced the currently running release, with the bundle assertions extended to cover what this deploy actually ships: the dbstat guard and the Claude entrypoint override. Both use grep -F so the regex literal is matched as text. This can be dropped again once the container path is proven. --- .github/workflows/standalone-build.yml | 137 +++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 .github/workflows/standalone-build.yml diff --git a/.github/workflows/standalone-build.yml b/.github/workflows/standalone-build.yml new file mode 100644 index 00000000000..0608d43d1e9 --- /dev/null +++ b/.github/workflows/standalone-build.yml @@ -0,0 +1,137 @@ +# Build the OmniRoute standalone bundle in CI instead of on the live router. +# +# The router (r8g.large, 2 vCPU / 16 GB aarch64) cannot build safely: an in-place +# build caused a ~30 min fleet outage on 2026-07-26. Runtime RSS is ~1.2 GB while +# the build peaks near 15 GB, and `build:release` deletes `.build` — which is the +# live systemd WorkingDirectory. +# +# This builds on a free arm64 runner (public repo), asserts the correct bundler, +# and uploads a tarball the host unpacks into a NEW release dir. + +name: Build Standalone Artifact + +on: + workflow_dispatch: + inputs: + ref: + description: "Branch/tag/SHA to build" + required: true + default: release/v3.8.50 + push: + branches: + - "release/v3.8.50" + +permissions: + contents: read + +concurrency: + group: standalone-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + # MUST match the router: Linux aarch64. A macOS build emits sharp-darwin-* + # and Darwin better_sqlite3.node, which will not load there. + runs-on: ubuntu-24.04-arm + timeout-minutes: 90 + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref || github.ref }} + persist-credentials: false + + - name: Report runner resources + run: | + echo "arch: $(uname -m) cores: $(nproc)" + free -h + + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + + # Mirrors the router runbook's documented flags exactly. + - name: Install dependencies + run: npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts + + - name: Rebuild native modules + run: npm rebuild better-sqlite3 + + # Call `npm run build` directly, never `build:release` — that script begins + # with `rm -rf .build dist`, which on the host deletes the running service's + # working directory. Here it would only be wasteful, but keeping the same + # command everywhere avoids the habit. + - name: Build + env: + OMNIROUTE_USE_TURBOPACK: "0" + NEXT_PUBLIC_BASE_URL: https://omniroute.technick.ai + NODE_OPTIONS: --max-old-space-size=8192 + run: | + set -o pipefail + OMNIROUTE_BUILD_SHA=$(git rev-parse --short HEAD) npm run build 2>&1 | tee /tmp/build.log + + # Load-bearing. The project's own release script has previously ignored + # OMNIROUTE_USE_TURBOPACK=0 and invoked Turbopack anyway, which OOMs the + # router. Fail here rather than ship that bundle. + - name: Assert the safe bundler was used + run: | + if grep -qi "turbopack" /tmp/build.log; then + echo "::error::Turbopack used — it OOMs the target host. Rejected." + grep -i turbopack /tmp/build.log | head -5 + exit 1 + fi + echo "OK: webpack build confirmed" + + - name: Build CLI + run: npm run build:cli + + # Catch platform and patch problems here, not on the production box. + - name: Verify artifact shape + run: | + fail=0 + [ -s .build/next/standalone/server.js ] || { echo "::error::missing server.js"; fail=1; } + if find .build -path "*sharp-darwin*" | grep -q .; then + echo "::error::darwin native modules present — wrong build platform"; fail=1 + fi + if find .build -name "*.node" | head -1 | grep -q .; then + echo "native modules present: $(find .build -name '*.node' | wc -l)" + else + echo "::error::no native modules in bundle"; fail=1 + fi + # Assert our fork's fixes actually survived into the bundle. + if grep -rqs "isStreamEarlyEof" .build/next/standalone 2>/dev/null; then + echo "OK: STREAM_EARLY_EOF breaker fix present in bundle" + else + echo "::error::fork fix (isStreamEarlyEof) missing from bundle"; fail=1 + fi + if grep -rqsF "no such (module|table): dbstat" .build/next/standalone 2>/dev/null; then + echo "OK: dbstat guard present in bundle" + else + echo "::error::dbstat guard missing from bundle"; fail=1 + fi + if grep -rqsF "CLAUDE_CC_ENTRYPOINT" .build/next/standalone 2>/dev/null; then + echo "OK: Claude entrypoint override present in bundle" + else + echo "::error::CLAUDE_CC_ENTRYPOINT missing from bundle"; fail=1 + fi + exit $fail + + - name: Package + id: pack + run: | + SHORT=$(git rev-parse --short HEAD) + NAME="standalone-${SHORT}" + tar --zstd -cf "${NAME}.tar.zst" .build dist + sha256sum "${NAME}.tar.zst" > "${NAME}.tar.zst.sha256" + ls -lh "${NAME}.tar.zst" + echo "name=${NAME}" >> "$GITHUB_OUTPUT" + + - uses: actions/upload-artifact@v4 + with: + name: ${{ steps.pack.outputs.name }} + path: | + ${{ steps.pack.outputs.name }}.tar.zst + ${{ steps.pack.outputs.name }}.tar.zst.sha256 + retention-days: 14 + compression-level: 0 From d01c7cc4bc1473f6c9e5df77a6b9054978479c8e Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 3 Aug 2026 18:56:36 -0500 Subject: [PATCH 4/5] ci: keep fork deployment automation in omniroute-ops --- .github/workflows/standalone-build.yml | 137 ------------------------- 1 file changed, 137 deletions(-) delete mode 100644 .github/workflows/standalone-build.yml diff --git a/.github/workflows/standalone-build.yml b/.github/workflows/standalone-build.yml deleted file mode 100644 index 0608d43d1e9..00000000000 --- a/.github/workflows/standalone-build.yml +++ /dev/null @@ -1,137 +0,0 @@ -# Build the OmniRoute standalone bundle in CI instead of on the live router. -# -# The router (r8g.large, 2 vCPU / 16 GB aarch64) cannot build safely: an in-place -# build caused a ~30 min fleet outage on 2026-07-26. Runtime RSS is ~1.2 GB while -# the build peaks near 15 GB, and `build:release` deletes `.build` — which is the -# live systemd WorkingDirectory. -# -# This builds on a free arm64 runner (public repo), asserts the correct bundler, -# and uploads a tarball the host unpacks into a NEW release dir. - -name: Build Standalone Artifact - -on: - workflow_dispatch: - inputs: - ref: - description: "Branch/tag/SHA to build" - required: true - default: release/v3.8.50 - push: - branches: - - "release/v3.8.50" - -permissions: - contents: read - -concurrency: - group: standalone-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - # MUST match the router: Linux aarch64. A macOS build emits sharp-darwin-* - # and Darwin better_sqlite3.node, which will not load there. - runs-on: ubuntu-24.04-arm - timeout-minutes: 90 - - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.ref || github.ref }} - persist-credentials: false - - - name: Report runner resources - run: | - echo "arch: $(uname -m) cores: $(nproc)" - free -h - - - uses: actions/setup-node@v4 - with: - node-version: "24" - cache: npm - - # Mirrors the router runbook's documented flags exactly. - - name: Install dependencies - run: npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts - - - name: Rebuild native modules - run: npm rebuild better-sqlite3 - - # Call `npm run build` directly, never `build:release` — that script begins - # with `rm -rf .build dist`, which on the host deletes the running service's - # working directory. Here it would only be wasteful, but keeping the same - # command everywhere avoids the habit. - - name: Build - env: - OMNIROUTE_USE_TURBOPACK: "0" - NEXT_PUBLIC_BASE_URL: https://omniroute.technick.ai - NODE_OPTIONS: --max-old-space-size=8192 - run: | - set -o pipefail - OMNIROUTE_BUILD_SHA=$(git rev-parse --short HEAD) npm run build 2>&1 | tee /tmp/build.log - - # Load-bearing. The project's own release script has previously ignored - # OMNIROUTE_USE_TURBOPACK=0 and invoked Turbopack anyway, which OOMs the - # router. Fail here rather than ship that bundle. - - name: Assert the safe bundler was used - run: | - if grep -qi "turbopack" /tmp/build.log; then - echo "::error::Turbopack used — it OOMs the target host. Rejected." - grep -i turbopack /tmp/build.log | head -5 - exit 1 - fi - echo "OK: webpack build confirmed" - - - name: Build CLI - run: npm run build:cli - - # Catch platform and patch problems here, not on the production box. - - name: Verify artifact shape - run: | - fail=0 - [ -s .build/next/standalone/server.js ] || { echo "::error::missing server.js"; fail=1; } - if find .build -path "*sharp-darwin*" | grep -q .; then - echo "::error::darwin native modules present — wrong build platform"; fail=1 - fi - if find .build -name "*.node" | head -1 | grep -q .; then - echo "native modules present: $(find .build -name '*.node' | wc -l)" - else - echo "::error::no native modules in bundle"; fail=1 - fi - # Assert our fork's fixes actually survived into the bundle. - if grep -rqs "isStreamEarlyEof" .build/next/standalone 2>/dev/null; then - echo "OK: STREAM_EARLY_EOF breaker fix present in bundle" - else - echo "::error::fork fix (isStreamEarlyEof) missing from bundle"; fail=1 - fi - if grep -rqsF "no such (module|table): dbstat" .build/next/standalone 2>/dev/null; then - echo "OK: dbstat guard present in bundle" - else - echo "::error::dbstat guard missing from bundle"; fail=1 - fi - if grep -rqsF "CLAUDE_CC_ENTRYPOINT" .build/next/standalone 2>/dev/null; then - echo "OK: Claude entrypoint override present in bundle" - else - echo "::error::CLAUDE_CC_ENTRYPOINT missing from bundle"; fail=1 - fi - exit $fail - - - name: Package - id: pack - run: | - SHORT=$(git rev-parse --short HEAD) - NAME="standalone-${SHORT}" - tar --zstd -cf "${NAME}.tar.zst" .build dist - sha256sum "${NAME}.tar.zst" > "${NAME}.tar.zst.sha256" - ls -lh "${NAME}.tar.zst" - echo "name=${NAME}" >> "$GITHUB_OUTPUT" - - - uses: actions/upload-artifact@v4 - with: - name: ${{ steps.pack.outputs.name }} - path: | - ${{ steps.pack.outputs.name }}.tar.zst - ${{ steps.pack.outputs.name }}.tar.zst.sha256 - retention-days: 14 - compression-level: 0 From aae942421c8be7c04fb81cfc4e6abc542a7f255b Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Mon, 3 Aug 2026 19:01:42 -0500 Subject: [PATCH 5/5] docs(test): register restored Claude env and dbstat coverage --- docs/reference/ENVIRONMENT.md | 1 + stryker.conf.json | 11 +++-------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 1bf2f0f0649..0d402ecc556 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -264,6 +264,7 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp | `OMNIROUTE_PAYLOAD_RULES_PATH` | `./config/payloadRules.json` | `open-sse/services/payloadRules.ts` | Path to payload manipulation rules JSON file (per-model/protocol upstream tweaks). | | `OMNIROUTE_PAYLOAD_RULES_RELOAD_MS` | `5000` | `open-sse/services/payloadRules.ts` | Reload interval (ms) for hot-reloading the payload rules file. Minimum `1000`. | | `OMNIROUTE_PREFER_CLAUDE_CODE_FOR_UNPREFIXED_CLAUDE_MODELS` | `false` | `open-sse/services/model.ts` | Opt-in: route bare `claude-*` model IDs from Claude Code clients through the Claude Code OAuth account instead of requiring a provider prefix. Explicit provider prefixes still win. Also configurable via a dashboard toggle on the Claude provider page. | +| `CLAUDE_CC_ENTRYPOINT` | `cli` | `open-sse/config/anthropicHeaders.ts` | Native Claude OAuth billing and User-Agent entrypoint. Accepted values: `cli` and `sdk-cli`. Invalid values fall back to `cli`. API-key requests retain the static `cli` identity. | --- diff --git a/stryker.conf.json b/stryker.conf.json index 1ba881ff051..2c10d25cc04 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -39,9 +39,7 @@ "incremental": true, "incrementalFile": "reports/mutation/stryker-incremental.json", "testRunner": "tap", - "plugins": [ - "@stryker-mutator/tap-runner" - ], + "plugins": ["@stryker-mutator/tap-runner"], "tap": { "testFiles": [ "tests/unit/7993-noauth-proxy-routing.test.ts", @@ -291,6 +289,7 @@ "tests/unit/sse-auth-antigravity-credits.test.ts", "tests/unit/sse-auth-resource-404.test.ts", "tests/unit/sse-auth.test.ts", + "tests/unit/db/stats-dbstat-optional.test.ts", "tests/unit/stream-early-eof-breaker.test.ts", "tests/unit/strict-random-deck.test.ts", "tests/unit/strip-reasoning-header.test.ts", @@ -417,11 +416,7 @@ ".worktrees", ".stryker-tmp" ], - "reporters": [ - "progress", - "html", - "json" - ], + "reporters": ["progress", "html", "json"], "htmlReporter": { "fileName": "reports/mutation/mutation.html" },