diff --git a/.env.example b/.env.example index e6c8f35c8f0..c1a1f06e674 100644 --- a/.env.example +++ b/.env.example @@ -1023,13 +1023,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500 # to disable the check. Used by: src/lib/db/migrationRunner.ts. Default: 50. #OMNIROUTE_MAX_PENDING_MIGRATIONS=50 -# Working directory for the check:install-upgrade release gate. It builds two ~3 GB -# install trees plus a ~275 MB tarball, so it needs roughly 12 GB — more than the -# 12 GB RAM-backed tmpfs that /tmp is on the self-hosted runner, where it exhausted -# the tmpfs and npm silently truncated the package. Defaults to /.install-upgrade -# on real disk. Used by: scripts/check/check-install-upgrade.mjs. Default: /.install-upgrade. -#OMNIROUTE_INSTALL_UPGRADE_WORKDIR=/var/tmp/omniroute-install-upgrade - # Trust user-managed RTK project filter rules without strict signature checks. # Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0. #OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34874b0c7fe..11e5b2f7d8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -976,7 +976,11 @@ jobs: # 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it); # merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive # release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16). - timeout-minutes: 20 + # 30, not 20 (2026-08-29): the informational Codecov upload below hung for the rest of + # the budget on two consecutive main runs (33207760653, 33215115341); the job ended + # `cancelled` and dragged the whole run's conclusion to `cancelled` although every + # blocking job was green. The upload step now has its own ceiling; this is headroom. + timeout-minutes: 30 needs: test-unit if: ${{ !cancelled() && needs.test-unit.result == 'success' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }} env: @@ -1055,6 +1059,10 @@ jobs: # (if-no-files-found: warn) — Sonar consumes the same file. - name: Upload coverage to Codecov (informational) if: always() + # Informational means informational: its own ceiling and continue-on-error, so a + # stalled upload can neither eat the job's budget nor turn a green job cancelled. + timeout-minutes: 5 + continue-on-error: true uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: coverage/lcov.info diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 97ace16b406..123c762d78a 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -485,6 +485,9 @@ jobs: severity: CRITICAL ignore-unfixed: true exit-code: "1" + # Explicit: the advisory scan above already points at it, and the blocking + # gate must honour the same accepted-risk list (#12084). + trivyignores: .trivyignore - name: Upload Trivy SARIF to Security tab if: needs.prepare.outputs.version != 'main' diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index e899a664eaa..856584ef3fd 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -4,12 +4,21 @@ on: push: tags: - "v*" + # A dispatch builds the ref it is dispatched ON (`gh workflow run … --ref v3.8.50` rebuilds + # that tag; `--ref main` builds the repaired line). The ref is deliberately NOT an input: + # CodeQL flags an input-controlled checkout next to the npm cache on the default branch as + # cache poisoning (actions/cache-poisoning/poisonable-step), and `github.ref` is trusted. workflow_dispatch: inputs: version: description: "Release version (e.g., v1.6.8)" required: true type: string + publish_npm: + description: "Also run the npm publish leg (turn off when re-attaching desktop assets to a release whose npm package already shipped)" + required: false + default: true + type: boolean # Least-privilege default: read-only at the top level; each job grants the writes it # needs (build/release upload assets, publish-npm forwards npm provenance / packages @@ -404,7 +413,14 @@ jobs: tag_name: ${{ needs.validate.outputs.version }} draft: false prerelease: false - generate_release_notes: true + # NEVER. Phase 3 of the release flow creates the GitHub Release with the curated + # notes seconds after pushing the tag, so by the time this step runs (1-2 h of + # builds later) the body already exists — and `true` APPENDS GitHub's + # auto-generated "What's Changed" block to it (v3.8.48 shipped that way; the + # v3.8.50 re-attach dispatch added +1,416 chars to a 121 KB body, run + # 33238093090). A curated body sits ~3 KB under the 125,000-char cap, so the + # append can also turn this step RED and leave the release with no assets. + generate_release_notes: false fail_on_unmatched_files: false files: | release-assets/*.dmg @@ -462,11 +478,20 @@ jobs: publish-npm: name: Publish to npm needs: [validate, release] + # A re-dispatch that only re-attaches desktop assets must not publish the npm package again. + if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_npm }} permissions: # Must be `write`, not `read`: this job calls the reusable npm-publish.yml whose # `publish` job needs `contents: write` (gh release upload — attach the SBOM, #3874). # A reusable workflow's job cannot request more permission than the caller grants, # so a `read` here makes GitHub reject the run at startup (startup_failure). + # + # `actions: read` for the same reason: the called `publish` job downloads the next-build + # artefact and requests it. v3.8.50 (run 33005490476) died at startup with "The nested + # job 'publish' is requesting 'actions: read', but is only allowed 'actions: none'" — and + # because `release` lives in this same workflow, the tag shipped with ZERO assets. Keep + # this block a superset of every job's permissions in npm-publish.yml. + actions: read contents: write id-token: write # npm provenance (forwarded to the reusable workflow) packages: write # publish to npm.pkg.github.com diff --git a/.github/workflows/nightly-release-green.yml b/.github/workflows/nightly-release-green.yml index 4aa3fec847b..92c734c48f3 100644 --- a/.github/workflows/nightly-release-green.yml +++ b/.github/workflows/nightly-release-green.yml @@ -196,6 +196,26 @@ jobs: gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md fi + - name: Close tracking issue when the branch is green again + if: steps.validate.outputs.exit == '0' + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ steps.branch.outputs.target }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + # The open/update step above is the UPWARD half of the loop; without this + # step a stale "not green" issue outlives the fix and every base-green check + # (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited. + TITLE="🔴 Release branch not green: ${TARGET}" + EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \ + --search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "") + if [ -n "$EXISTING" ]; then + gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \ + --comment "✅ \`${TARGET}\` is release-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)." + echo "Closed issue #$EXISTING" + fi + - name: Upload report artifact if: always() uses: actions/upload-artifact@v7 @@ -294,6 +314,25 @@ jobs: gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md fi + - name: Close tracking issue when the branch is green again + if: steps.validate.outputs.exit == '0' + env: + GH_TOKEN: ${{ github.token }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + # The open/update step above is the UPWARD half of the loop; without this + # step a stale "not green" issue outlives the fix and every base-green check + # (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited. + TITLE="🔴 main branch not green" + EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \ + --search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "") + if [ -n "$EXISTING" ]; then + gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \ + --comment "✅ \`main\` is main-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)." + echo "Closed issue #$EXISTING" + fi + - name: Upload report artifact if: always() uses: actions/upload-artifact@v7 diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index c34dd6e0144..5ba76f069fc 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -273,11 +273,20 @@ jobs: if-no-files-found: error - name: Attach SBOM to GitHub Release - if: steps.resolve.outputs.skip != 'true' && github.event_name == 'release' + # Not only on the `release` event: the v3.8.50 package shipped through a + # workflow_dispatch (staged publish, 11 attempts) and this step was skipped, so the + # GitHub Release carried no SBOM until it was attached by hand from the run's + # `sbom-npm` artifact. Attach whenever a release for the published tag exists. + if: steps.resolve.outputs.skip != 'true' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ github.ref_name }} - run: gh release upload "$TAG" sbom-npm.cdx.json --clobber + TAG: ${{ github.event_name == 'release' && github.ref_name || format('v{0}', inputs.version) }} + run: | + if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "::notice::no GitHub Release for $TAG yet — SBOM stays on the sbom-npm workflow artifact" + exit 0 + fi + gh release upload "$TAG" sbom-npm.cdx.json --repo "$GITHUB_REPOSITORY" --clobber # WS1.2/WS1.3 (#7065 class): the artifact that is about to be published must # BOOT. build:cli already assembled dist/ above; this packs+installs+boots the diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a0b371bd9b5..0f46beefbeb 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -4,12 +4,15 @@ on: schedule: - cron: "27 7 * * 1" push: - branches: ["main"] + # Scorecard only accepts the DEFAULT branch — here the active release/vX.Y.Z, + # not `main`. The job below guards on it so a push to any other branch skips. + branches: ["main", "release/**"] permissions: read-all jobs: analysis: + if: ${{ github.event_name != 'push' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} name: Scorecard analysis runs-on: ubuntu-latest permissions: diff --git a/.gitignore b/.gitignore index 31c9e3b96a2..07545f2a37f 100644 --- a/.gitignore +++ b/.gitignore @@ -293,6 +293,3 @@ docker-compose.yml.bak # Ad-hoc test sandboxes (never tracked — may contain local DBs) /.sandbox/ .aider* - -# check:install-upgrade work trees (~12 GB, disposable) -/.install-upgrade/ diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 3371ff78f3f..00000000000 --- a/.mailmap +++ /dev/null @@ -1,40 +0,0 @@ -# .mailmap — canonical author identities for git log/shortlog/blame. -# -# Why this file exists: between 2026-08-13 and 2026-08-26 this checkout carried a -# `git config --local` whose user.name was one contributor's ("Xiangzhe" / @xz-dev) -# and whose user.email was ANOTHER contributor's (@backryun). Every commit produced -# on this machine in that window was therefore signed with @backryun's address — -# 237 commits, all in the -0300 timezone, while @backryun's own work commits from -# +0900 and continued normally throughout. The local override was removed on -# 2026-08-26; this file repairs the RECORD without rewriting published history -# (those commits live on release/v3.8.50 and release/v3.8.51, which other sessions -# and open PRs build on — a rewrite would force-push both and orphan the v3.8.50 tag). -# -# Format: Canonical Name Commit Name - -# --- Maintainer: several addresses used over the project's life --- -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Diego Souza <8016841+diegosouzapw@users.noreply.github.com> - -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> - -# --- The misattribution window: name Xiangzhe + @backryun's email, from -0300. -# These are maintainer/session commits, NOT @backryun's contributions. -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Xiangzhe -diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Xiangzhe - -# --- Xiangzhe (@xz-dev) — a distinct contributor; keep their own work intact --- -Xiangzhe <32761048+xz-dev@users.noreply.github.com> -Xiangzhe <32761048+xz-dev@users.noreply.github.com> - -# --- @backryun's own alternate addresses (their real work, kept intact) --- -backryun <24198422+backryun@users.noreply.github.com> -backryun <24198422+backryun@users.noreply.github.com> -backryun <24198422+backryun@users.noreply.github.com> -backryun <24198422+backryun@users.noreply.github.com> diff --git a/AGENTS.md b/AGENTS.md index eccff635582..320b71dc049 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below. | Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) | | Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions | | Services | `open-sse/services/` | Combo routing, rate limits, caching, etc | -| Database | `src/lib/db/` | SQLite domain modules (160 migrations) | +| Database | `src/lib/db/` | SQLite domain modules (159 migrations) | | Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic | | MCP Server | `open-sse/mcp-server/` | 110 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes | | A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol | diff --git a/CHANGELOG.md b/CHANGELOG.md index 61df3f2666c..e755f273ee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,67 +93,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `ed2db6cb19` → tip). Bullets carry the merged PR and its author; direct pushes listed separately._ -### 📊 Release by the numbers - -| | | -| --- | ---: | -| 👥 People who contributed | **248** | -| 📝 Commits in the cycle | **1,714** | -| 🔀 Pull requests referenced | **1,666** | -| 📋 Changelog entries | **1,182** | -| 🙌 Contributors credited in entries | **256** | -| 🤖 Automated dependency commits | 22 | - -**Entries by type** - -| Type | Count | -| --- | ---: | -| 🐛 Fixes | 779 | -| ✨ Features | 169 | -| 📚 Docs | 29 | -| 🧹 Chore | 27 | -| 🧪 Tests | 15 | -| ♻️ Refactor | 5 | -| ⚡ Performance | 3 | -| providers | 2 | -| 🔒 Security | 2 | -| ⚙️ CI | 2 | -| deps | 2 | -| maint | 2 | - -### 🏆 Top 25 contributors this cycle - -_By commits in `ed2db6cb19..v3.8.50`, author identities consolidated via `.mailmap`. Bots excluded._ - -| # | Contributor | Commits | -| ---: | --- | ---: | -| 🥇 | diegosouzapw | 738 | -| 🥈 | backryun | 88 | -| 🥉 | Dizzle | 66 | -| 4 | Ravi Tharuma | 52 | -| 5 | Markus Hartung | 48 | -| 6 | Bob.Hou | 42 | -| 7 | Rouzbeh† | 38 | -| 8 | Xiangzhe | 31 | -| 9 | Paco Cartones | 28 | -| 10 | Nguyen Thanh Dat | 23 | -| 11 | Aman | 22 | -| 12 | Will Gordon | 19 | -| 13 | 小妍儿 ✨ | 17 | -| 14 | adevwithpurpose | 16 | -| 15 | Andrew B. | 10 | -| 16 | NOXX - Commiter | 10 | -| 17 | ignamiranda | 10 | -| 18 | Jonathan Bailey | 9 | -| 19 | Ke Jin | 9 | -| 20 | Austin Liu | 8 | -| 21 | Chewji | 8 | -| 22 | Prudhvi Vuda | 7 | -| 23 | benzntech | 7 | -| 24 | rinseaid | 7 | -| 25 | stanley | 7 | - - ### ✨ New Features - **feat(search):** first-class X Search provider (`x-search`) on `POST /v1/search` and MCP `omniroute_x_search` using SuperGrok / xAI server-side `x_search`. Explicit provider or `search_type: "x"` only — never auto-selected for web. Reuses `xai-oauth` / `xao` / `xai` credentials. Not the X Developer Platform MCP. ([#10985](https://github.com/diegosouzapw/OmniRoute/issues/10985)) - **feat(core):** add Layer A capability filter at router (#5696) diff --git a/README.md b/README.md index ade1c6b6562..aca28600b8d 100644 --- a/README.md +++ b/README.md @@ -1202,7 +1202,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi RuntimeNode.js 22.x / 24.x LTS — >=22.22.2 <23 || >=24.0.0 <27 LanguageTypeScript 6.0 — 100% TypeScript across src/ and open-sse/ (zero any in core since v2.0) FrameworkNext.js 16 + React 19 + Tailwind CSS 4 - Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 160 migrations + Databasebetter-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 159 migrations MemorySQLite FTS5 full-text + int8-quantized vector embeddings, typed decay SchemasZod 4 — MCP tool I/O validation + API contracts ProtocolsMCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE) diff --git a/changelog.d/features/npm-trusted-publishing-oidc.md b/changelog.d/features/npm-trusted-publishing-oidc.md deleted file mode 100644 index 8452c0c0b3d..00000000000 --- a/changelog.d/features/npm-trusted-publishing-oidc.md +++ /dev/null @@ -1,4 +0,0 @@ -- The npm publish is automatic again, through npm Trusted Publishing (OIDC): the hosted - `stage-npm` job publishes with a short-lived credential minted from GitHub's id-token — - no `NPM_TOKEN`, no 2FA prompt, provenance attached. `publish_mode=staged` (owner - approves with 2FA) and `direct` (token) remain available on `workflow_dispatch`. diff --git a/changelog.d/fixes/11741-install-upgrade-schema-convergence.md b/changelog.d/fixes/11741-install-upgrade-schema-convergence.md deleted file mode 100644 index 6bdf6e846ac..00000000000 --- a/changelog.d/fixes/11741-install-upgrade-schema-convergence.md +++ /dev/null @@ -1,8 +0,0 @@ -- **fix(db):** `model_capabilities` is created by a migration instead of lazily on the first - models.dev sync, so a clean install and an upgraded install converge on the same schema - regardless of which features have run -- **fix(ci):** `check:install-upgrade` now fails on an `npm` install truncated by ENOSPC - (npm reports it as a warning and still exits 0), authenticates its health probe so the - version assertion works against the hardened health payload, frees the clean-install tree - before the upgrade phase, and no longer reports a schema divergence computed from a boot - that never served diff --git a/changelog.d/fixes/11856-npm-payload-nft-manifests.md b/changelog.d/fixes/11856-npm-payload-nft-manifests.md deleted file mode 100644 index 0420d6f9c0b..00000000000 --- a/changelog.d/fixes/11856-npm-payload-nft-manifests.md +++ /dev/null @@ -1,4 +0,0 @@ -- Excluded Next.js Node File Trace manifests (`*.nft.json`) from the published npm - tarball. They are build-time metadata and are never read while serving, but had - grown to 668.7 MB — 61% of the package — which pushed the upload past the - registry limit and made `npm publish` fail with `413 Payload Too Large`. diff --git a/changelog.d/fixes/11866-alibaba-allowlist-test-timebomb.md b/changelog.d/fixes/11866-alibaba-allowlist-test-timebomb.md deleted file mode 100644 index e448bb8ed86..00000000000 --- a/changelog.d/fixes/11866-alibaba-allowlist-test-timebomb.md +++ /dev/null @@ -1,5 +0,0 @@ -- Fixed the Alibaba free-tier allowlist test that went red on its own once the - shipped catalog's `validUntil` (2026-08-27) passed, leaving every PR and `main` - with a failing `Unit Tests (1/8)`. The test now builds its own packs with dates - it controls, and covers the expired-pack fallback that production has actually - been serving. diff --git a/changelog.d/fixes/11868-npm-provenance-hosted-runner.md b/changelog.d/fixes/11868-npm-provenance-hosted-runner.md deleted file mode 100644 index 4cc94111ce6..00000000000 --- a/changelog.d/fixes/11868-npm-provenance-hosted-runner.md +++ /dev/null @@ -1,4 +0,0 @@ -- Split the npm registry upload into its own GitHub-hosted job. npm refuses - `--provenance` from a self-hosted runner (`422 ... Only "github-hosted" runners - are supported`), which blocked the v3.8.50 publish; the heavy verification - cannot move to a hosted runner, so it now hands the proven tarball over instead. diff --git a/changelog.d/maintenance/11866-config-expiry-time-bomb-test.md b/changelog.d/maintenance/11866-config-expiry-time-bomb-test.md deleted file mode 100644 index 9e58941cfc2..00000000000 --- a/changelog.d/maintenance/11866-config-expiry-time-bomb-test.md +++ /dev/null @@ -1,4 +0,0 @@ -- Added a unit test that fails seven days before any dated pack under `config/` - (`validUntil` and sibling keys) lapses, naming the file and key. The Alibaba - free-tier pack expired on 2026-08-27 and turned every PR red the next morning - with no commit involved; renewal now happens on someone's terms, not the clock's. diff --git a/changelog.d/maintenance/11878-check-workflows-provenance-self-hosted.md b/changelog.d/maintenance/11878-check-workflows-provenance-self-hosted.md deleted file mode 100644 index d07e0fdbdc3..00000000000 --- a/changelog.d/maintenance/11878-check-workflows-provenance-self-hosted.md +++ /dev/null @@ -1,3 +0,0 @@ -- `check:workflows` now fails (under `--strict`/`--ratchet`) when any job routed to a - self-hosted runner publishes with `--provenance` — npm rejects that with `422` at the - registry, which in v3.8.50 only surfaced after the tag and Docker images were public. diff --git a/changelog.d/maintenance/11892-runner-janitor-act-not-advise.md b/changelog.d/maintenance/11892-runner-janitor-act-not-advise.md deleted file mode 100644 index 4f311fdf298..00000000000 --- a/changelog.d/maintenance/11892-runner-janitor-act-not-advise.md +++ /dev/null @@ -1,5 +0,0 @@ -- `scripts/ops/runner-janitor.sh` now proves a path is idle with one `lsof` - snapshot and removes stale leftovers itself (tmpfs after 3 h — it is RAM — disk - after 24 h), kills orphan `next-build` processes, prunes checkouts of stopped - runners, and alerts on memory pressure; `--dry-run` shows exactly what it would - do. `docs/ops/RUNNER_BOX.md` reconciled to the measured box (31 GB, 10 listeners). diff --git a/changelog.d/maintenance/11896-ci-artifact-download-to-disk.md b/changelog.d/maintenance/11896-ci-artifact-download-to-disk.md deleted file mode 100644 index bceb4cf8346..00000000000 --- a/changelog.d/maintenance/11896-ci-artifact-download-to-disk.md +++ /dev/null @@ -1,5 +0,0 @@ -- The `next-build` artefact (1.3 GB) is now written and read under `$RUNNER_TEMP` - (per-runner, on disk) instead of `/tmp`, which on the self-hosted pool is a - 12 GB tmpfs in RAM. Landing it there took 27–32 of the publish job's 76 minutes, - and the fixed `/tmp/e2e-build.tar.gz` name let E2E jobs on different runners - overwrite each other's download. diff --git a/changelog.d/maintenance/11897-ci-heavy-build-lane.md b/changelog.d/maintenance/11897-ci-heavy-build-lane.md deleted file mode 100644 index 6bb3bbee3b5..00000000000 --- a/changelog.d/maintenance/11897-ci-heavy-build-lane.md +++ /dev/null @@ -1,3 +0,0 @@ -- The CI `build` job now runs in two concurrency lanes — `main` and pull requests — - so a release build is never queued behind (or OOM-killed beside) PR builds on the - self-hosted pool, which holds one `next-build` comfortably and two at the edge. diff --git a/changelog.d/maintenance/ci-omni-build-runner-label.md b/changelog.d/maintenance/ci-omni-build-runner-label.md deleted file mode 100644 index 50dfd41d243..00000000000 --- a/changelog.d/maintenance/ci-omni-build-runner-label.md +++ /dev/null @@ -1,4 +0,0 @@ -- Every CI job that runs a `next build` (`build`, the npm `publish`, both release-green - validations) now targets the `omni-build` runner label, which only two of the eight - self-hosted runners carry. The box holds one build comfortably and two at the edge; a - third now queues on GitHub instead of being OOM-killed by the kernel. diff --git a/config/quality/install-upgrade-allowlist.json b/config/quality/install-upgrade-allowlist.json index 0f6145223a1..4e6f3c6891c 100644 --- a/config/quality/install-upgrade-allowlist.json +++ b/config/quality/install-upgrade-allowlist.json @@ -1,6 +1,6 @@ { - "_doc": "Tables that exist ONLY in databases upgraded from an older version. Two causes, and they call for different fixes: (a) residue whose CREATE left the migration set in some past cycle but survives where it already existed — allowlist it here; (b) a table created LAZILY at runtime with `CREATE TABLE IF NOT EXISTS` inside a feature code path — whether a database has it depends on whether that feature ran, so it diverges by TIMING and can show up on EITHER side. Fix (b) with a migration instead of an entry here (see src/lib/db/migrations/163_model_capabilities.sql); an allowlist entry only hides it in one direction. Either way, recorded so check-install-upgrade.mjs can still fail on a NEW divergence. The opposite direction (a table a clean install creates but an upgrade does not) is NEVER allowlisted: it means every existing user is missing structure the code expects.", + "_doc": "Tables that exist ONLY in databases upgraded from an older version — residue whose CREATE left the migration set in some past cycle but survives where it already existed. Harmless (nothing references them), but recorded here so check-install-upgrade.mjs can still fail on a NEW divergence. The opposite direction (a table a clean install creates but an upgrade does not) is NEVER allowlisted: it means every existing user is missing structure the code expects.", "residualTables": { - "cache_metrics": "Measured 2026-07-30 on a real 3.8.48 install upgraded to 3.8.49 (VPS .16, 165 MB database, 114 → 117 tables). Present in upgraded databases, absent from clean installs. Cause identified 2026-08-27: it is case (b) above — created lazily by `ensureCacheMetricsTable()` at src/lib/semanticCache.ts:34, never by a migration, so it appears only where the semantic cache has run. No code path referenced it during the upgrade (zero `no such table` in 150 log lines, both installs healthy). Left as an allowlist entry rather than promoted to a migration or dropped: unlike model_capabilities it did not block a release, and creating a table for a subsystem we cannot prove is live is not a change to make blind. Revisit when the cache subsystem is next touched." + "cache_metrics": "Measured 2026-07-30 on a real 3.8.48 install upgraded to 3.8.49 (VPS .16, 165 MB database, 114 → 117 tables). Present in upgraded databases, absent from clean installs. No code path referenced it during the upgrade (zero `no such table` in 150 log lines, both installs healthy). Left in place rather than dropped: a DROP migration on a table we cannot prove is unused everywhere is the riskier change. Revisit when the cache subsystem is next touched." } } diff --git a/config/quality/quality-baseline.json b/config/quality/quality-baseline.json index 5f426893468..2edc9b51e6b 100644 --- a/config/quality/quality-baseline.json +++ b/config/quality/quality-baseline.json @@ -169,7 +169,7 @@ "dedicatedGate": true }, "zizmorFindings": { - "value": 194, + "value": 192, "_rebaseline_2026_08_20_radar_export_workflow": "190 -> 192 (+2). Workflow novo `.github/workflows/radar-export.yml` (passo 10 do go-live do Radar: publica o export estável do catálogo como asset de release para o servidor privado baixar via RADAR_EXPORT_URL). Os +2 são unpinned-uses @vN: actions/checkout@v7 + actions/setup-node@v7 — a MESMA convenção deliberada de todos os workflows (ver _scanner_harden_workflows_2026_06_16); fixar por SHA só este violaria a convenção. O findings artipacked do checkout foi CORRIGIDO com `persist-credentials: false` (o job publica via GH_TOKEN em `gh release`, não usa a credencial do checkout). Nenhuma classe nova de template-injection / cache-poisoning / dangerous-triggers. Medido local com zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 191; +1 do delta conhecido do runner (ver _rebaseline_2026_07_28_ci_runner_delta: o runner enxerga 1 unpinned-uses @vN a mais que o devbox no mesmo commit; a baseline segue o runner) => 192.", "_rebaseline_2026_07_20_aliasresolver_hook_split_7808": "175 -> 176 (+1). Companion to PR #7808 (CodeQL js/incomplete-url-substring-sanitization fix in bin/aliasResolver.mjs). The +1 is NOT caused by this PR's code changes (bin/* is not a workflow file) — it is a pre-existing drift that surfaced because the ratchet gate runs on this PR's CI: the zizmor scanner version on the GitHub runner gained a new rule (or extended an existing one) since the v3.8.49 baseline was seeded on 2026-07-17. Breakdown: the new finding is an unpinned-uses @vN class item on one of the existing workflows (same deliberate convention as _scanner_harden_workflows_2026_06_16 — @vN is intentional, SHA-pinning only this one would violate the convention). No new template-injection/artipacked/cache-poisoning/dangerous-triggers classes introduced. Measured by the Quality Gates (Extended) job on run 29713001401 = 176, baseline was 175. Note: by the time this landed on release/v3.8.49, the baseline was already at 176 via _rebaseline_2026_07_17_combo_recovery_hints — this entry is kept as historical record; no further bump applied.", "_rebaseline_2026_07_17_v3849_release": "169 -> 175 (+6). Cycle workflow drift (v3.8.48/v3.8.49): npm-publish.yml (new, WS1.3 #7092), electron-release.yml, nightly-compat.yml, nightly-release-green.yml, CI restructures (#7501 full-history base fetch, #7355 main-green, #7202 merge-queue gates, Trunk/Codecov). Breakdown vs v3.8.47: +3 unpinned-uses (@vN convention, deliberate per _scanner_harden_workflows_2026_06_16), +2 cache-poisoning (artifact upload/cache in the OWN electron-release/npm-publish RELEASE workflows -- operator-controlled, not fork-PR exploitable), +1 excessive-permissions (nightly-compat.yml permissions:issues). No new template-injection/artipacked/dangerous-triggers. Measured with zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 175 on da3a0be69.", @@ -180,8 +180,7 @@ "_rebaseline_2026_06_23_v3834_release": "152 -> 155 (+3). The 3 new unpinned-uses are in .github/workflows/nightly-release-green.yml (added by #4622 this cycle): actions/checkout@v7, actions/setup-node@v6, actions/upload-artifact@v4 — the SAME deliberate @vN convention as ci.yml's own checkout@v7/setup-node@v6 and every other workflow (see _scanner_harden_workflows_2026_06_16 + _zizmor_rebaseline_2026_06_20_ci_build_artifact_reuse). SHA-pinning only this workflow would violate the convention. The workflow-lint ratchet does NOT run on PR->release fast-gates, so it surfaced only on the release PR; measured locally via `npm run check:workflows -- --ratchet` = 155. No new template-injection/artipacked/cache-poisoning.", "_rebaseline_2026_07_13_v3847_release_preflight": "159 -> 169 (+10). Findings from cycle-merged workflow changes: #6716 (PR gate restructure), #6781 (unit fast-path shard 2->4), #6788 (TIA tsx loader split), #6881 (electron-updater latest.yml manifests in release assets) — same deliberate @vN unpinned-uses convention as prior rebaselines; no new template-injection/artipacked/cache-poisoning classes. Measured via `npm run check:workflows -- --ratchet` = 169 on the v3.8.47 release pre-flight.", "_rebaseline_2026_07_28_v3849_release_preflight": "176 -> 189 (+13). Pre-flight de fechamento da v3.8.49 (934 commits no ciclo). Deriva de workflow: 1 workflow novo (build-rinseaid-image.yml) mais os bumps de action do Dependabot ao longo do ciclo — todos da MESMA classe unpinned-uses @vN, convenção deliberada do repo (ver _scanner_harden_workflows_2026_06_16); fixar por SHA só estes violaria a convenção. Nenhuma classe nova de template-injection / artipacked / cache-poisoning / dangerous-triggers. Nesta mesma passada foram CORRIGIDAS 3 diretivas shellcheck malformadas (SC1125: `# shellcheck disable=SC2086 — texto`, em que o travessão invalida o par key=value) em ci.yml e nightly-release-green.yml. Medido com zizmor 1.25.2 via `npm run check:workflows -- --ratchet` = 189.", - "_rebaseline_2026_07_28_ci_runner_delta": "189 -> 190 (+1). Medido 189 no devbox e 190 no runner do GitHub no MESMO commit (run 30396592013, job Quality Gates (Extended)) — mesma classe já registrada em _rebaseline_2026_07_20_aliasresolver_hook_split_7808: a versão do zizmor no runner enxerga uma finding a mais que a local, sempre da classe unpinned-uses @vN. O valor do runner é o que o gate compara, então a baseline segue o runner.", - "_rebaseline_2026_08_28_npm_publish_hosted_stage_job": "192 -> 194 (+2). Job novo `stage-npm` em .github/workflows/npm-publish.yml: o npm RECUSA `--provenance` vindo de runner self-hosted (422 \"Unsupported GitHub Actions runner environment\"), e o job `publish` nao pode migrar para runner hospedado porque 16 GB nao bastam para o fallback next-build do build:cli (documentado no proprio runs-on). A separacao foi a unica saida que preserva a atestacao SLSA que a 3.8.49 ja tem. Os +2 sao da MESMA convencao deliberada de todos os workflows (ver _scanner_harden_workflows_2026_06_16): unpinned-uses @vN em actions/download-artifact@v8 + actions/setup-node@v7, mais o cache-poisoning que o proprio setup-node@v7 ja gera nos outros 2 jobs deste MESMO arquivo (linhas 85 e 463) e que ja esta na baseline. Fixar por SHA so este job violaria a convencao. Nenhuma classe nova: zero template-injection / artipacked / dangerous-triggers / excessive-permissions — o job declara apenas contents:read + id-token:write, que e o minimo para a proveniencia. Medido pelo job Quality Gates (Extended) no run 33162... da PR #11877 = 194." + "_rebaseline_2026_07_28_ci_runner_delta": "189 -> 190 (+1). Medido 189 no devbox e 190 no runner do GitHub no MESMO commit (run 30396592013, job Quality Gates (Extended)) — mesma classe já registrada em _rebaseline_2026_07_20_aliasresolver_hook_split_7808: a versão do zizmor no runner enxerga uma finding a mais que a local, sempre da classe unpinned-uses @vN. O valor do runner é o que o gate compara, então a baseline segue o runner." }, "vulnCount": { "value": 22, diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index 7257f316223..418b6f63b05 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index 4add0bed9d1..4396f903bda 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index 4add0bed9d1..4396f903bda 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index 24cff0c716f..a57fc49fb82 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index 0594a702256..81c5dbe221f 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index ae6db801f63..3a11dfe9f21 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index 7ec4db9296c..a773d356539 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index 4060c00fc73..f613f9fb953 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index ebbf822ca45..16ca289f0bf 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index df997d94295..464931cfaad 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index 7700b3e5274..4d24695fe9b 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index 40625789325..d29ff85617f 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index ccb265b5fe8..d502286fcea 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index 871e39c4ddb..581991636b6 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index b81f3aa36b6..83ad7575a2b 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index f07509d70d7..0df19edabf0 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 3b535f7af57..8ab7e12163b 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index 86b75c3934c..568027ff193 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 84a6e203c6d..de0a53dcdd7 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index 8cd426d9fe6..ab77dce63d3 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index 1c91c72a574..284c8e44d38 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index bb20db5bdbf..6e5e7fa7f2f 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index c8f37e5e098..c1cc74d2bb8 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index 1ad6b6c901f..bf413cf3d4a 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index 266050c0558..f2f4d98e932 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index c1b4e0ee0a5..10cdafa4d9e 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index d67b8635cb7..4025212393c 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index 97d5b179fbe..66409df5792 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index 23b2fbbb28a..20bb796c42f 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index f8485462087..cb1ec554e7b 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index b96048893b2..d33730ab7f4 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index d5e46fc0181..a7ef78f6914 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index 1c916465fd8..1bcbbb9ea91 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index e520ad067ac..daffa944091 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index 8a42e1cf4af..79093481184 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index 686d8e6357b..5a5d8ffaf54 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index ade80042b55..d9124fede5a 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index 5c7ebf1b026..946eb2cd998 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index 797c2cb36b3..3840a4ad157 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index 2b8eddeeaf7..3e07f942d0f 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index acbef1dd9de..ab11d68462a 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index fa9dd4261d7..268343a9d26 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/docs/ops/RELEASE_CHECKLIST.md b/docs/ops/RELEASE_CHECKLIST.md index 53fa6bb7333..dfa96d53eed 100644 --- a/docs/ops/RELEASE_CHECKLIST.md +++ b/docs/ops/RELEASE_CHECKLIST.md @@ -1,12 +1,12 @@ --- title: "Release Checklist" -version: 3.8.51 -lastUpdated: 2026-08-28 +version: 3.8.40 +lastUpdated: 2026-06-28 --- # Release Checklist -> **Last updated:** 2026-08-28 — v3.8.51 +> **Last updated:** 2026-06-28 — v3.8.40 > Streamlined release flow that leverages Claude Code skills for automation. > > **Keep the queue/branch green between releases:** see [RELEASE_GREEN.md](./RELEASE_GREEN.md) @@ -37,21 +37,7 @@ npm run test:e2e # optional but recommended /capture-release-evidences-cc ``` -## npm Trusted Publishing (default since v3.8.51) — staged on request, direct as fallback - -`npm-publish.yml` publishes through **npm Trusted Publishing (OIDC)** by default: the -`stage-npm` job (github-hosted) exchanges GitHub's id-token for a short-lived npm -credential for that run — no long-lived npm token in the repository secrets, no 2FA prompt, provenance attached. -That is the bypass npm sanctions now that tokens which skip 2FA are being retired; -it restores the fully automatic flow the project had up to v3.8.48 while keeping the -WS1.3 guarantee (a leaked token cannot publish alone — there is no token). - -**One-time setup (owner):** npmjs.com → package `omniroute` → Settings → *Trusted -Publisher* → GitHub: owner `diegosouzapw`, repo `OmniRoute`, workflow `npm-publish.yml` -(environment: none). Until that exists, the automatic step fails with `ENEEDAUTH`: -re-dispatch with `publish_mode=staged` (below) or `direct`. - -### Staged publishing (on request — `publish_mode=staged`) +## npm Staged Publishing (default since v3.8.49 — WS1.3/D2) The npm-publish workflow no longer publishes directly: it boots the packed tarball (`check:pack-boot`) and then runs `npm stage publish` — the exact bytes are parked on diff --git a/docs/ops/RUNNER_BOX.md b/docs/ops/RUNNER_BOX.md index ce77f66f14b..07bd70cb045 100644 --- a/docs/ops/RUNNER_BOX.md +++ b/docs/ops/RUNNER_BOX.md @@ -4,66 +4,32 @@ title: Self-Hosted Runner Box Operations # Self-Hosted Runner Box Operations (.113 pool) -The self-hosted pool (`self-hosted, omni-release` on all eight runners; `omni-build` on two) runs on the **.113** box. -Measured 2026-08-28 (v3.8.50 postmortem, Parte III): +The self-hosted pool (`self-hosted, omni-release` labels) runs on the 16 GB box at +`192.168.0.113`. Two failure modes recurred on release days and were, until v3.8.49, +manual discipline; the **janitor script codifies them** (WS3.3 of the quality plan): -| resource | value | what it means for scheduling | -| --------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| RAM / CPU | **31 GB / 32 cores** (was 16 GB when this doc was first written) | one `next-build` peaks at **~14 GB** → 2 concurrent heavy builds saturate the box, 3 take it down (2026-08-28 06:42Z: load 56, two jobs lost) | -| swap | 15 GB | it swapped its way through the v3.8.50 publish; pressure shows in `/proc/pressure/memory` | -| `/tmp` | **12 GB tmpfs = RAM** | anything parked there is memory; leftovers are swept after 3 h | -| disk | 188 GB | `_work` checkouts of 8 runners reach ~70 GB with no cap | -| runners | **10 listeners**: 8 OmniRoute + OmniHeuris + OmniMind | all share the memory above | +1. **Orphaned temp/work dirs** filling the disk → disk-full SQLite errors mid-job. +2. **>4 concurrent runners** → OOM-killed jobs (8-wide killed jobs twice on the + v3.8.47 release day; 4-wide is the proven ceiling). ## Install the janitor (one-time, on the box) ```bash -scp scripts/ops/runner-janitor.sh root@192.168.0.113:/opt/omniroute-ops/runner-janitor.sh -ssh root@192.168.0.113 'chmod +x /opt/omniroute-ops/runner-janitor.sh; apt-get install -y lsof' -# cron (root): every 30 min, log to /var/log/runner-janitor.log -*/30 * * * * MAX_ACTIVE_RUNNERS=8 /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1 +sudo mkdir -p /opt/omniroute-ops +sudo cp scripts/ops/runner-janitor.sh /opt/omniroute-ops/ +sudo chmod +x /opt/omniroute-ops/runner-janitor.sh +( sudo crontab -l 2>/dev/null; echo '*/30 * * * * /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1' ) | sudo crontab - ``` -`lsof` is required: the janitor proves a path is idle with one snapshot of open -files before removing it, and without the tool it removes nothing and says so -(exit 1). Try any change with `--dry-run` first — it prints exactly what it would -do and touches nothing. - -What it does every run: sweeps our own leftovers (`runner-*`, `omniroute-*`, -`next-build*`, `e2e-build.tar.gz`) after **3 h on tmpfs** and 24 h on disk -`_work/_temp`; kills a `next-build` older than 75 min (no job runs that long — on -2026-08-27 one ran 70 min after GitHub had declared its job lost); prunes 48 h-old -checkouts of runners whose unit is **stopped**; alerts on disk ≥ 85 %, memory PSI -`full/avg60` ≥ 10 %, and more listeners than `MAX_ACTIVE_RUNNERS` (with an -omniroute/other breakdown). Exit 1 = attention needed; read the log. - -## Runner units: KillMode - -The runner's default `KillMode=process` leaves `Runner.Worker → npm → next-build` -alive when a unit is stopped or restarted — an orphan build keeps eating RAM and -CPU with no job attached. Every OmniRoute unit carries a drop-in -(`/etc/systemd/system/actions.runner.diegosouzapw-OmniRoute..service.d/10-killmode.conf`) -with `KillMode=mixed`: SIGTERM to the listener first, SIGKILL to the whole cgroup at -`TimeoutStop`. It takes effect on the unit's next restart — restart **one runner at -a time, only when idle**, with the idle check and the restart in the same command. +What it does every 30min: sweeps runner temp leftovers older than 24h, alerts at +≥85% root-disk usage, and alerts when more than the runner ceiling (default 4, tunable +via the script's own environment) of `Runner.Listener` processes are up. Alerts land in `/var/log/runner-janitor.log` +with a non-zero exit (grep for `⚠`). ## Operating rules -- **Heavy-build ceiling: 2 at a time — enforced by label.** Every job that runs a - `next build` (`ci.yml` `build`, `npm-publish.yml` `publish`, both `nightly-release-green` - validations) targets `[self-hosted, omni-build]`, and only **two** runners carry that - label (`omniroute-113-5`, `omniroute-113-6`, added through the runners API — no - re-registration). The other six keep `omni-release` and take nothing heavy; GitHub - queues a third build instead of the kernel killing one. Pair with the `heavy-build-*` - concurrency lanes in `ci.yml`. To add capacity, label another runner — never raise - the count past what 31 GB holds (one next-build ≈ 14–16 GB). -- **Never clean `/tmp` or `_work` by hand while any runner is busy.** A - check-then-delete with a gap between the two is how a live Build job lost its - `_work` on 2026-08-27. The janitor does the check and the removal in one step; - let it. -- Stopping a runner mid-job cancels the job (observed live): `systemctl stop` only - when its listener has no `Runner.Worker` child — and do it in one command. -- Workflows must not park artefacts in `/tmp` (it is RAM). Download to - `$RUNNER_TEMP` (on disk, per runner) — the 1.3 GB `next-build` artefact took 27–32 - minutes to land on the tmpfs and 2 minutes to upload from disk. +- **Ceiling: 4 runners** on the 16 GB box. Runners 5–8 stay STOPPED except for + explicit off-peak experiments — never during a release window. +- Stopping a runner mid-job cancels the job (observed live): `systemctl stop` + only when its runner is idle (`Runner.Listener` without a `Runner.Worker` child). - The `.15` VPS is homologation-only — never runs CI runners. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 72db4a2aae1..b383b5696fe 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -103,7 +103,6 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari | `OMNIROUTE_MIGRATIONS_DIR` | _(auto-detect)_ | `src/lib/db/migrationRunner.ts` | Override the directory that the migration runner scans. Useful when shipping bundled migrations in custom builds. | | `OMNIROUTE_EXTRA_MIGRATIONS_DIRS` | _(unset)_ | `src/lib/db/migrationRunner/extraDirs.ts` | Additional migration directories as `namespace=dir` entries separated by the platform path delimiter (e.g. `ee=/opt/app/enterprise/db/migrations`). Files found there are recorded as `-`, so a distribution shipping its own migrations never collides with the upstream numeric slots. A malformed entry, an invalid namespace or a missing directory throws at startup instead of silently skipping the schema. | | `OMNIROUTE_MAX_PENDING_MIGRATIONS` | `50` | `src/lib/db/migrationRunner.ts` | Mass-pending-migrations safety threshold (#3416). Startup aborts if more than this many migrations are pending on an existing DB (guards against a wiped tracking table). Raise it to restore an older backup; set to `0` to disable the check. | -| `OMNIROUTE_INSTALL_UPGRADE_WORKDIR` | _(`/.install-upgrade`)_ | `scripts/check/check-install-upgrade.mjs` | Working directory for the `check:install-upgrade` release gate. It needs roughly 12 GB (two ~3 GB install trees plus the tarball), so it must not run on a small tmpfs — on the self-hosted runner `/tmp` is a 12 GB RAM-backed tmpfs and the gate exhausted it, truncating the package. | | `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Flush interval (ms) for the batched spend/cost writer. Lower values reduce write coalescing; higher values reduce DB contention. | | `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Max buffered spend entries before a forced flush. Raise on high-QPS deployments; lower when bounded memory matters more. | | `OMNIROUTE_PROXY_FETCH_DEBUG` | _(unset)_ | `open-sse/utils/proxyFetch.ts` | Set to `"true"` to emit `[ProxyFetch]` debug logs on the Vercel relay path. Off by default to avoid leaking routing hints. | diff --git a/llm.txt b/llm.txt index 4a39efcb614..ac94ef46e44 100644 --- a/llm.txt +++ b/llm.txt @@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo - **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`) - **Framework:** Next.js 16 (App Router) with TypeScript 6 -- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations) +- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations) - **State management:** Zustand (client), SQLite (server persistence) - **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons - **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth @@ -434,7 +434,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool 4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`. -5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. +5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module. 6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches. diff --git a/package.json b/package.json index 152f897ce25..54a0bc149ed 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,7 @@ "!**/*.test.js", "!**/*.test.mjs", "!**/*.spec.ts", - "!**/*.spec.tsx", - "!**/*.nft.json" + "!**/*.spec.tsx" ], "workspaces": [ "open-sse", diff --git a/scripts/check/check-install-upgrade.mjs b/scripts/check/check-install-upgrade.mjs index a71ccdb1fe5..87253c611db 100644 --- a/scripts/check/check-install-upgrade.mjs +++ b/scripts/check/check-install-upgrade.mjs @@ -30,12 +30,10 @@ * Requires `npm run build:cli` first — this is a --with-build gate, like check:pack-boot. */ -import { execFileSync, spawn, spawnSync } from "node:child_process"; -import crypto from "node:crypto"; +import { execFileSync, spawn } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { pathToFileURL } from "node:url"; import { DatabaseSync } from "node:sqlite"; const BOOT_DEADLINE_MS = 180_000; @@ -45,59 +43,6 @@ const ALLOWLIST_PATH = "config/quality/install-upgrade-allowlist.json"; const log = (msg) => console.log(`[install-upgrade] ${msg}`); const warn = (msg) => console.log(`[install-upgrade] ⚠️ ${msg}`); -/** Root of the installed package inside an `npm install -g --prefix` tree. */ -function packageRootFor(prefix) { - return path.join(prefix, "lib", "node_modules", "omniroute"); -} - -/** - * Credential for the health probe. - * - * GHSA-mvf8-qc78-5mxm hardened /api/monitoring/health: an ANONYMOUS caller now gets only - * `{ status }` — the version, node version, pid and provider config are reserved for a - * management principal (src/app/api/monitoring/health/route.ts → publicHealthView). An - * unauthenticated probe therefore reads `body.version === undefined`, and this gate's - * version assertion could never pass again; the v3.8.50 publish run failed with - * "clean: health reports version undefined, expected 3.8.50" for exactly that reason. - * - * The gate spawns the server itself, so it can mint the credential instead of guessing one: - * `OMNIROUTE_INTERNAL_SERVICE_TOKEN` + the `x-omniroute-internal-service-token` header is - * accepted by requireManagementAuth() via isTrustedLoopbackInternalServiceRequest(), and the - * probe is loopback by construction. Unlike the machine token `check:pack-boot` derives, this - * does not depend on a readable machine-id, and an older PREVIOUS version that never gated - * health simply ignores the header. The assertion keeps its full strength — it just presents - * a credential. - */ -const INTERNAL_SERVICE_TOKEN = crypto.randomBytes(32).toString("hex"); -const INTERNAL_SERVICE_HEADER = "x-omniroute-internal-service-token"; - -/** - * Secondary credential: the same loopback machine token `check:pack-boot` derives from the - * packaged CLI. Sent alongside the internal-service token so a build that only honours one - * of the two still answers with the full payload. - */ -function derivePackagedCliToken(prefix) { - const cliModuleUrl = pathToFileURL( - path.join(packageRootFor(prefix), "bin", "cli", "utils", "cliToken.mjs") - ).href; - try { - return execFileSync( - process.execPath, - [ - "--input-type=module", - "--eval", - "import(process.argv[1]).then(async m => process.stdout.write(await m.getCliToken()))", - cliModuleUrl, - ], - { encoding: "utf8", env: { ...process.env } } - ).trim(); - } catch { - // A truncated/broken install cannot derive a token. Returning null keeps the boot - // probe running (it will fail loudly on its own) instead of crashing the gate here. - return null; - } -} - function pickTarball(packJson) { const filename = JSON.parse(packJson)?.[0]?.filename; if (!filename) throw new Error("npm pack --json returned no filename"); @@ -176,11 +121,6 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) { if (!fs.existsSync(binPath)) { return { ok: false, failures: [`${label}: bin not found at ${binPath}`], tail: [] }; } - const cliToken = derivePackagedCliToken(prefix); - const probeHeaders = { - [INTERNAL_SERVICE_HEADER]: INTERNAL_SERVICE_TOKEN, - ...(cliToken ? { "x-omniroute-cli-token": cliToken } : {}), - }; const child = spawn(binPath, ["serve", "--port", String(port)], { env: { ...process.env, @@ -190,7 +130,6 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) { API_KEY_SECRET: "install-upgrade-gate-api-key-secret-long", DISABLE_SQLITE_AUTO_BACKUP: "true", OMNIROUTE_SKIP_SYSTEM_TRUST: "1", - OMNIROUTE_INTERNAL_SERVICE_TOKEN: INTERNAL_SERVICE_TOKEN, }, stdio: ["ignore", "pipe", "pipe"], detached: true, @@ -212,33 +151,20 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) { let result = { ok: false, failures: [`${label}: never became healthy`], tail }; while (Date.now() < deadline) { if (childExit !== null) { - result = { - ok: false, - failures: [`${label}: exited with code ${childExit} before serving`], - tail, - }; + result = { ok: false, failures: [`${label}: exited with code ${childExit} before serving`], tail }; break; } try { - const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`, { - headers: probeHeaders, - }); + const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`); const body = await res.json().catch(() => null); if (res.status === 200 && body && typeof body === "object") { const failures = []; // `status` may legitimately report degraded (no providers configured) — the gate // targets boot crashes and version mismatches, not health of a bare install. - const reportedVersion = body.version ?? body.system?.version; - if (expectVersion && reportedVersion !== expectVersion) { - failures.push( - `${label}: health reports version ${reportedVersion}, expected ${expectVersion}` + - (reportedVersion === undefined - ? " — the payload carries no version at all, which is the ANONYMOUS health " + - "view: the probe's credentials were not accepted (see GHSA-mvf8-qc78-5mxm)" - : "") - ); + if (expectVersion && body.version !== expectVersion) { + failures.push(`${label}: health reports version ${body.version}, expected ${expectVersion}`); } - result = { ok: failures.length === 0, version: reportedVersion, failures, tail }; + result = { ok: failures.length === 0, version: body.version, failures, tail }; break; } } catch { @@ -257,72 +183,13 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) { return result; } -/** - * `npm install` reports ENOSPC as a *warning* per failed tar entry and still exits 0. - * - * That is not a theoretical concern: on the v3.8.50 publish run the Phase B upgrade install - * emitted 5611 `npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device` lines, exited - * 0, and left a truncated package behind. `omniroute serve` then "exited with code 0 before - * serving", no migration ever ran, and the gate concluded the release was missing 15 tables - * — a full false alarm produced by a full disk. Each install tree is ~3 GB, and the run - * builds two of them plus a ~275 MB tarball. - * - * So: surface the truncation at the install, where it is unambiguous. - */ -function npmInstallInto(prefix, spec, label = spec) { - // spawnSync (not execFileSync): execFileSync forwards the child's stderr straight to the - // parent's, so the ENOSPC warnings scrolled past in CI without the script ever seeing - // them. spawnSync hands both streams back. - const run = spawnSync( - "npm", - ["install", "-g", "--prefix", prefix, "--no-audit", "--no-fund", spec], - { encoding: "utf8", maxBuffer: 512 * 1024 * 1024 } - ); - const output = `${run.stdout ?? ""}${run.stderr ?? ""}`; - // Keep the install log visible, but a truncated package emits thousands of identical - // warnings — collapse them so the real message is not buried. - const stderrLines = String(run.stderr ?? "").split("\n"); - const shown = stderrLines.length > 60 ? stderrLines.slice(0, 40) : stderrLines; - if (String(run.stderr ?? "").trim()) { - process.stderr.write(shown.join("\n") + "\n"); - if (stderrLines.length > 60) { - process.stderr.write(`[install-upgrade] … ${stderrLines.length - 40} more npm line(s)\n`); - } - } - assertNoDiskExhaustion(output, label); - if (run.error) throw run.error; - if (run.status !== 0) { - throw new Error(`${label}: npm install exited with code ${run.status}`); - } -} - -export function assertNoDiskExhaustion(output, label) { - if (!/ENOSPC|no space left on device/i.test(output)) return; - const count = (output.match(/ENOSPC/g) ?? []).length; - throw new Error( - `${label}: the install ran out of disk space (${count} ENOSPC error(s) from npm). ` + - `The package tree is truncated, so anything measured from it — boot, schema, ` + - `migrations — is meaningless. Free space in ${workDirForMessages} (each install tree is ` + - `~3 GB) and re-run. This is an environment failure, NOT a schema divergence.` - ); -} - -/** Best-effort free bytes on the filesystem backing `dir`, or null when unavailable. */ -function freeBytes(dir) { - try { - return fs.statfsSync(dir).bavail * fs.statfsSync(dir).bsize; - } catch { - return null; - } +function npmInstallInto(prefix, spec) { + execFileSync("npm", ["install", "-g", "--prefix", prefix, "--no-audit", "--no-fund", spec], { + encoding: "utf8", + maxBuffer: 128 * 1024 * 1024, + }); } -const GB = 1024 ** 3; - -// Set once the work directory exists, so the ENOSPC message names the filesystem that -// actually ran out — pointing at /tmp when the gate works elsewhere sends the reader to -// free space on the wrong volume (which is what happened during the v3.8.50 publish). -let workDirForMessages = os.tmpdir(); - function resolvePreviousVersion(current, explicit) { if (explicit) return explicit; const out = execFileSync("npm", ["view", "omniroute", "dist-tags.latest"], { encoding: "utf8" }); @@ -331,9 +198,7 @@ function resolvePreviousVersion(current, explicit) { if (latest === current) { // The version under test is already published (re-run of a shipped release): step back // to the highest published version strictly below it. - const all = JSON.parse( - execFileSync("npm", ["view", "omniroute", "versions", "--json"], { encoding: "utf8" }) - ); + const all = JSON.parse(execFileSync("npm", ["view", "omniroute", "versions", "--json"], { encoding: "utf8" })); const stable = all.filter((v) => !/-(rc|alpha|beta|pre|next)/.test(v) && v !== current); return stable[stable.length - 1]; } @@ -353,25 +218,11 @@ async function main() { } const version = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version; const allowlist = loadAllowlist(ROOT); - // NOT os.tmpdir(): on the self-hosted runner /tmp is a 12 GB tmpfs backed by RAM, while - // the root filesystem has ~66 GB free. This gate needs ~12 GB, so it exhausted the tmpfs - // and npm truncated the package — 58269 ENOSPC errors on the v3.8.50 publish, which the - // previous code could only report as a crash. Freeing disk did not help because the disk - // was never the constraint. Work on real disk beside the repo instead. - const workRoot = - process.env.OMNIROUTE_INSTALL_UPGRADE_WORKDIR || path.join(ROOT, ".install-upgrade"); - fs.mkdirSync(workRoot, { recursive: true }); - const tmp = fs.mkdtempSync(path.join(workRoot, "omniroute-install-upgrade-")); - workDirForMessages = tmp; + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-install-upgrade-")); const failures = []; const warnings = []; try { - // Timed, because this turned out to be the expensive part: on the 2026-08-27 - // v3.8.50 publish `npm pack` alone took 24m37s, leaving 5 of the step's 30-minute - // budget for two installs and two boots. Without a duration here the log showed - // only "packing…" then a timeout, which reads like a hang and is not. - const packStarted = Date.now(); log(`packing v${version}…`); const packOut = execFileSync("npm", ["pack", "--json", "--pack-destination", tmp], { cwd: ROOT, @@ -379,31 +230,13 @@ async function main() { maxBuffer: 128 * 1024 * 1024, }); const tarball = path.join(tmp, pickTarball(packOut)); - const packMb = (fs.statSync(tarball).size / 1024 / 1024).toFixed(1); - log(`packed in ${Math.round((Date.now() - packStarted) / 1000)}s (${packMb} MB)`); - - // Each install tree is ~3 GB and this run builds two of them, side by side, plus the - // ~275 MB tarball. On the v3.8.50 publish run that overflowed the runner disk mid-way - // through the Phase B upgrade install; npm warned per truncated tar entry and still - // exited 0, and every later measurement was taken from a broken tree. - const availableBytes = freeBytes(tmp); - if (availableBytes !== null) { - log(`free space in ${tmp}: ${(availableBytes / GB).toFixed(1)} GB`); - if (availableBytes < 12 * GB) { - warn( - `only ${(availableBytes / GB).toFixed(1)} GB free — this gate needs roughly 12 GB ` + - `(two ~3 GB install trees, the second installed over twice, plus the tarball). ` + - `An install truncated by ENOSPC looks like a schema divergence.` - ); - } - } // ---- Phase A: clean install ------------------------------------------------- log("PHASE A — clean install of the packed tarball"); const aPrefix = path.join(tmp, "a-prefix"); const aData = path.join(tmp, "a-data"); fs.mkdirSync(aData, { recursive: true }); - npmInstallInto(aPrefix, tarball, "clean install"); + npmInstallInto(aPrefix, tarball); const a = await bootAndProbe({ prefix: aPrefix, dataDir: aData, @@ -412,34 +245,14 @@ async function main() { label: "clean", }); failures.push(...a.failures); - const cleanBooted = a.ok; if (a.ok) log(`clean install healthy on v${a.version}`); - else if (a.tail?.length) { - console.error("[install-upgrade] last output from the clean-install server:"); - console.error(a.tail.join("").split("\n").slice(-40).join("\n")); - } const aDb = findDb(aData); const freshTables = aDb ? readTables(aDb) : null; if (!freshTables) failures.push("clean: no SQLite database was created"); else log(`clean install schema: ${freshTables.size} tables`); - // Phase A is fully measured (boot verdict + schema snapshot); its ~3 GB install tree is - // dead weight from here on and Phase B needs the room. The DATA_DIR stays — only the - // node_modules tree goes. - if (!skipUpgrade) { - fs.rmSync(aPrefix, { recursive: true, force: true }); - const reclaimed = freeBytes(tmp); - log( - "released the clean-install tree before the upgrade phase" + - (reclaimed !== null ? ` (${(reclaimed / GB).toFixed(1)} GB free)` : "") - ); - } - // ---- Phase B: upgrade over the previous published version ------------------- let upgradedTables = null; - // `--skip-upgrade` never reaches the convergence block (upgradedTables stays null), so - // defaulting this to true keeps that path unchanged. - let upgradeBooted = true; if (skipUpgrade) { warn("PHASE B skipped (--skip-upgrade)"); } else { @@ -449,7 +262,7 @@ async function main() { const bData = path.join(tmp, "b-data"); fs.mkdirSync(bData, { recursive: true }); - npmInstallInto(bPrefix, `omniroute@${previous}`, `previous(${previous}) install`); + npmInstallInto(bPrefix, `omniroute@${previous}`); const before = await bootAndProbe({ prefix: bPrefix, dataDir: bData, @@ -460,16 +273,14 @@ async function main() { if (!before.ok) { // A broken PREVIOUS version is not this release's fault — degrade to a warning so a // historically bad publish cannot block the current one. - warnings.push( - `previous version ${previous} did not boot cleanly — upgrade path unverified` - ); + warnings.push(`previous version ${previous} did not boot cleanly — upgrade path unverified`); for (const f of before.failures) warn(f); } else { const beforeDb = findDb(bData); const beforeTables = beforeDb ? readTables(beforeDb) : new Set(); log(`previous(${previous}) schema: ${beforeTables.size} tables — upgrading in place`); - npmInstallInto(bPrefix, tarball, "upgrade install"); + npmInstallInto(bPrefix, tarball); const after = await bootAndProbe({ prefix: bPrefix, dataDir: bData, @@ -479,11 +290,6 @@ async function main() { }); failures.push(...after.failures); if (after.ok) log(`upgrade healthy on v${after.version}`); - upgradeBooted = after.ok; - if (!after.ok && after.tail?.length) { - console.error("[install-upgrade] last output from the upgraded server:"); - console.error(after.tail.join("").split("\n").slice(-40).join("\n")); - } const afterDb = findDb(bData); upgradedTables = afterDb ? readTables(afterDb) : null; @@ -500,19 +306,7 @@ async function main() { } // ---- Schema convergence ----------------------------------------------------- - // Only meaningful when BOTH servers actually served. A boot that died before serving - // never ran a migration, so its database still holds the PREVIOUS release's schema and - // every post-baseline table shows up as "a clean install creates but an upgrade does - // not" — which is what the v3.8.50 publish run reported after ENOSPC truncated the - // upgrade install. Comparing there does not add information, it manufactures a - // 15-table false alarm on top of the real failure. The run still fails: the boot - // failure is already in `failures`. - if (freshTables && upgradedTables && !(cleanBooted && upgradeBooted)) { - warn( - "schema convergence NOT evaluated — a phase failed to boot, so its database was " + - "never migrated and any table difference would describe the broken boot, not the schema" - ); - } else if (freshTables && upgradedTables) { + if (freshTables && upgradedTables) { const verdict = evaluateConvergence({ freshTables, upgradedTables, @@ -540,10 +334,7 @@ async function main() { // Only run the (expensive) gate when invoked directly — importing this module for the pure // helper above must not pack, install or boot anything. -if ( - process.argv[1] && - path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname) -) { +if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) { main().catch((err) => { console.error(`[install-upgrade] crashed: ${err?.message ?? err}`); process.exit(1); diff --git a/scripts/check/check-workflows.mjs b/scripts/check/check-workflows.mjs index 8559332fce0..2ac213ff37b 100644 --- a/scripts/check/check-workflows.mjs +++ b/scripts/check/check-workflows.mjs @@ -42,7 +42,6 @@ import { execFileSync, spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import { findProvenanceOnSelfHosted, formatProvenanceFinding } from "./lib/provenanceRunner.mjs"; const ROOT = process.cwd(); const WORKFLOWS_DIR = path.join(ROOT, ".github", "workflows"); @@ -276,23 +275,6 @@ export function runZizmor(workflowsDir) { // Main // --------------------------------------------------------------------------- -/** - * Hard rule (not a lint count): `--provenance` inside a job that runs on a - * self-hosted runner. npm answers 422 at the registry, and in v3.8.50 that - * answer only came after the tag, the GitHub Release and the Docker images were - * already out. Blocks under --strict AND --ratchet (the CI mode); plain mode - * reports it like everything else. - * @param {string[]} files absolute workflow paths - */ -export function runProvenanceRunnerCheck(files) { - const findings = []; - for (const file of files) { - const text = fs.readFileSync(file, "utf8"); - findings.push(...findProvenanceOnSelfHosted(text, path.relative(ROOT, file))); - } - return findings; -} - function main() { const hasActionlint = isBinaryAvailable("actionlint"); const hasZizmor = isBinaryAvailable("zizmor"); @@ -368,16 +350,6 @@ function main() { } } - const provenanceFindings = runProvenanceRunnerCheck(workflowFiles); - if (provenanceFindings.length > 0) { - console.error( - `[check-workflows] provenance×self-hosted: ${provenanceFindings.length} finding(s) — HARD RULE:` - ); - provenanceFindings.forEach((f) => console.error(` ${formatProvenanceFinding(f)}`)); - } else if (!QUIET) { - console.log("[check-workflows] provenance×self-hosted: OK (0 findings)"); - } - const total = actionlintCount + zizmorCount; process.stdout.write(`workflowFindings=${total}\n`); process.stdout.write(`actionlintFindings=${actionlintCount}\n`); @@ -385,15 +357,6 @@ function main() { // Read this line with the count above: a finding total is only reproducible against the // version that produced it. See zizmorVersion(). process.stdout.write(`zizmorVersion=${hasZizmor ? zizmorVersion() : "absent"}\n`); - process.stdout.write(`provenanceRunnerFindings=${provenanceFindings.length}\n`); - if ((STRICT || RATCHET) && provenanceFindings.length > 0) { - console.error( - `\n[check-workflows] FAIL — ${provenanceFindings.length} job(s) publish with --provenance from a self-hosted runner.\n` + - " npm rejects that with 422 at the registry. Move the upload step to a github-hosted job\n" + - " (see .github/workflows/npm-publish.yml `stage-npm` for the pattern)." - ); - process.exit(1); - } if (STRICT && total > 0) { console.error(`\n[check-workflows] FAIL — ${total} workflow finding(s) total (--strict mode).`); diff --git a/scripts/check/lib/configExpiry.mjs b/scripts/check/lib/configExpiry.mjs deleted file mode 100644 index 110a998c66e..00000000000 --- a/scripts/check/lib/configExpiry.mjs +++ /dev/null @@ -1,90 +0,0 @@ -/** - * scripts/check/lib/configExpiry.mjs - * - * Finds dated validity fields in JSON config packs so a test can fail BEFORE - * they lapse. Origin: config/alibaba-free-tier-allowlist.json carried - * `"validUntil": "2026-08-27"`; on 2026-08-28 the loader started (correctly) - * rejecting the pack and a test that asserted "the shipped pack loads" turned - * every PR and main red with no commit involved (#11866). A time bomb, not a - * regression — and the only kind of defect a diff review can never catch. - * - * Pure helpers; the repo-wide assertion lives in - * tests/unit/config-expiry-time-bomb.test.ts. - */ -import fs from "node:fs"; -import path from "node:path"; - -export const EXPIRY_KEY = - /^(validUntil|valid_until|validTo|valid_to|expiresAt|expires_at|expiry|expires)$/; -const DAY_MS = 86_400_000; - -/** - * Walks a parsed JSON value and returns every string-valued expiry field. - * @returns {{ file: string, keyPath: string, raw: string, expiresAt: number|null }[]} - */ -export function collectExpiryFields(value, file, keyPath = []) { - const out = []; - if (Array.isArray(value)) { - value.forEach((v, i) => out.push(...collectExpiryFields(v, file, [...keyPath, String(i)]))); - return out; - } - if (!value || typeof value !== "object") return out; - for (const [key, v] of Object.entries(value)) { - const kp = [...keyPath, key]; - if (EXPIRY_KEY.test(key) && typeof v === "string") { - const ms = Date.parse(v); - out.push({ file, keyPath: kp.join("."), raw: v, expiresAt: Number.isFinite(ms) ? ms : null }); - } else if (v && typeof v === "object") { - out.push(...collectExpiryFields(v, file, kp)); - } - } - return out; -} - -/** @returns {"expired"|"expiring"|"ok"|"unparseable"} */ -export function classifyExpiry(field, nowMs, warnDays = 7) { - if (field.expiresAt === null) return "unparseable"; - if (field.expiresAt < nowMs) return "expired"; - if (field.expiresAt < nowMs + warnDays * DAY_MS) return "expiring"; - return "ok"; -} - -/** All *.json under dir, recursively, skipping node_modules. Sorted for stable output. */ -export function walkJsonFiles(dir) { - const out = []; - const stack = [dir]; - while (stack.length > 0) { - const current = stack.pop(); - let entries; - try { - entries = fs.readdirSync(current, { withFileTypes: true }); - } catch { - continue; - } - for (const e of entries) { - const full = path.join(current, e.name); - if (e.isDirectory()) { - if (e.name !== "node_modules") stack.push(full); - } else if (e.isFile() && e.name.endsWith(".json")) { - out.push(full); - } - } - } - return out.sort(); -} - -/** - * Scans every JSON file under `dir`; `file` in the result is relative to `dir` - * with forward slashes, so allowlists can key on it portably. - */ -export function scanConfigExpiry(dir) { - return walkJsonFiles(dir).flatMap((f) => { - let parsed; - try { - parsed = JSON.parse(fs.readFileSync(f, "utf8")); - } catch { - return []; // not this scanner's job to validate JSON - } - return collectExpiryFields(parsed, path.relative(dir, f).split(path.sep).join("/")); - }); -} diff --git a/scripts/check/lib/provenanceRunner.mjs b/scripts/check/lib/provenanceRunner.mjs deleted file mode 100644 index b38602e96bd..00000000000 --- a/scripts/check/lib/provenanceRunner.mjs +++ /dev/null @@ -1,83 +0,0 @@ -/** - * scripts/check/lib/provenanceRunner.mjs - * - * npm refuses `--provenance` from a self-hosted runner: - * - * 422 Unprocessable Entity - Error verifying sigstore provenance bundle: - * Unsupported GitHub Actions runner environment: "self-hosted". - * Only "github-hosted" runners are supported when publishing with provenance. - * - * v3.8.50 hit this at the very end of a 76-minute publish job — after the tag, - * the GitHub Release and the Docker images were already public — because - * `USE_VPS_RUNNER` had been turned on (2026-08-02) with no release in between to - * surface it. The combination is greppable, so it must fail in CI the moment a - * workflow introduces it, not four weeks later at the registry. - * - * Pure: takes workflow YAML text, returns the offending (job, step) pairs. - */ -import { load as yamlLoad } from "js-yaml"; - -const SELF_HOSTED = /\bself-hosted\b/; -const EXPRESSION = /\$\{\{/; -// Lookahead, not \b: `--provenance-file=…` is a different flag (a pre-built -// bundle) and must not match — a word boundary sits between "e" and "-". -const PROVENANCE = /(^|\s)--provenance(?=\s|=|$)/m; - -/** - * Classifies a job's `runs-on` value. - * @returns {"self-hosted"|"hosted"|"unknown"} - * "unknown" = an expression with no literal `self-hosted` in it (e.g. - * `${{ matrix.os }}`); the check does not guess, it skips. - */ -export function classifyRunsOn(runsOn) { - if (runsOn == null) return "unknown"; - if (typeof runsOn === "string") { - if (SELF_HOSTED.test(runsOn)) return "self-hosted"; - return EXPRESSION.test(runsOn) ? "unknown" : "hosted"; - } - if (Array.isArray(runsOn)) { - return runsOn.some((v) => typeof v === "string" && SELF_HOSTED.test(v)) - ? "self-hosted" - : "hosted"; - } - if (typeof runsOn === "object") { - // { group: ..., labels: ... } form - const labels = runsOn.labels; - return classifyRunsOn(Array.isArray(labels) ? labels : labels == null ? "" : String(labels)); - } - return "unknown"; -} - -/** - * @param {string} yamlText - * @param {string} fileName used only for reporting - * @returns {{ file: string, job: string, step: string }[]} - */ -export function findProvenanceOnSelfHosted(yamlText, fileName = "") { - let doc; - try { - doc = yamlLoad(yamlText); - } catch { - // actionlint owns syntax; an unparseable file is not this rule's finding. - return []; - } - const jobs = - doc && typeof doc === "object" && doc.jobs && typeof doc.jobs === "object" ? doc.jobs : {}; - const findings = []; - for (const [jobName, job] of Object.entries(jobs)) { - if (!job || typeof job !== "object") continue; - if (classifyRunsOn(job["runs-on"]) !== "self-hosted") continue; - const steps = Array.isArray(job.steps) ? job.steps : []; - steps.forEach((step, i) => { - if (step && typeof step.run === "string" && PROVENANCE.test(step.run)) { - findings.push({ file: fileName, job: jobName, step: step.name || `#${i + 1}` }); - } - }); - } - return findings; -} - -/** Human-readable line per finding, used by the CLI. */ -export function formatProvenanceFinding(f) { - return `${f.file}: job "${f.job}", step "${f.step}" runs \`--provenance\` on a self-hosted runner — npm rejects that (422). Move the upload to a github-hosted job.`; -} diff --git a/scripts/ops/runner-janitor.sh b/scripts/ops/runner-janitor.sh index 9a07cf0b605..99c081b10f3 100755 --- a/scripts/ops/runner-janitor.sh +++ b/scripts/ops/runner-janitor.sh @@ -1,172 +1,53 @@ #!/usr/bin/env bash -# runner-janitor — self-hosted runner box hygiene for the .113 pool. +# runner-janitor — self-hosted runner box hygiene (WS3.3, v3.8.49 quality plan). # -# Runs from cron every 30 min (see docs/ops/RUNNER_BOX.md). It ACTS on what it -# can prove is safe and ALERTS on what needs an operator decision. Reads of -# "is this in use?" and the removal happen in the same command, never in two -# passes: a check-then-delete with a gap is how a live Build job lost its _work -# on 2026-08-27. +# The .113 runner box has recurring failure modes that until now were manual +# discipline: orphaned tmpfs/work dirs filling the disk, and >4 concurrent +# runners OOM-killing jobs (16 GB box; incidents on the v3.8.47 release day). +# Install via cron on the box (see docs/ops/RUNNER_BOX.md): +# */30 * * * * /opt/omniroute-ops/runner-janitor.sh >> /var/log/runner-janitor.log 2>&1 # -# Measured box (2026-08-28): 31 GB RAM, 32 cores, 15 GB swap, /tmp = 12 GB -# tmpfs (RAM!), 188 GB disk. A single `next-build` peaks at ~14 GB, so two -# concurrent heavy builds saturate the box and three take it down (06:42Z that -# day: load 56, two jobs lost). The v3.8.50 postmortem (Parte III) has the numbers. -# -# What it does, in order: -# 1) sweep stale artefacts our tooling leaves behind — tmpfs bases after 3 h -# (they hold RAM), disk _work/_temp bases after 24 h; only names we create, -# only when no process has them open -# 2) kill zombie builds: a `next-build` older than ZOMBIE_BUILD_MAX_MIN has no -# job attached (a real Build step measures ~26 min). On 2026-08-27 one ran -# 70 minutes after GitHub had already declared its job lost, eating 3.6 GB -# and a full core set. KillMode=mixed on the units covers systemctl -# stop/restart; this covers the lost-connection path. -# 3) prune 48 h-old checkouts under _work of runners whose unit is INACTIVE -# (stopped runners cannot be mid-job; active ones are never touched) -# 4) alert: root disk >= DISK_ALERT_PCT, memory PSI full/avg60 >= threshold, -# Runner.Listener count above the ceiling (with a per-project breakdown — -# the box also hosts OmniHeuris and OmniMind runners) -# -# Usage: runner-janitor.sh [--dry-run] [--help] # Exit codes: 0 healthy · 1 attention needed (printed to stdout for the log). set -euo pipefail -DRY_RUN=0 -for arg in "$@"; do - case "$arg" in - --dry-run) DRY_RUN=1 ;; - -h|--help) - sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//' - exit 0 ;; - *) echo "unknown argument: $arg" >&2; exit 2 ;; - esac -done - -MAX_ACTIVE_RUNNERS="${MAX_ACTIVE_RUNNERS:-8}" +MAX_ACTIVE_RUNNERS="${MAX_ACTIVE_RUNNERS:-4}" DISK_ALERT_PCT="${DISK_ALERT_PCT:-85}" -TMPFS_MAX_AGE_HOURS="${TMPFS_MAX_AGE_HOURS:-3}" -WORK_TEMP_MAX_AGE_HOURS="${WORK_TEMP_MAX_AGE_HOURS:-24}" -WORK_CHECKOUT_MAX_AGE_HOURS="${WORK_CHECKOUT_MAX_AGE_HOURS:-48}" -ZOMBIE_BUILD_MAX_MIN="${ZOMBIE_BUILD_MAX_MIN:-75}" -ZOMBIE_BUILD_COMM="${ZOMBIE_BUILD_COMM:-next-build}" -PSI_FULL_AVG60_ALERT="${PSI_FULL_AVG60_ALERT:-10}" -# Overridable so the unit test can point everything at a fixture tree. -JANITOR_TMP_BASES="${JANITOR_TMP_BASES-/tmp}" -JANITOR_WORK_TEMP_BASES="${JANITOR_WORK_TEMP_BASES-/opt/actions-runner*/_work/_temp /home/*/actions-runner*/_work/_temp}" -JANITOR_RUNNER_DIRS="${JANITOR_RUNNER_DIRS-/opt/actions-runner*}" -JANITOR_PSI_FILE="${JANITOR_PSI_FILE:-/proc/pressure/memory}" -JANITOR_DF_PATH="${JANITOR_DF_PATH:-/}" - +WORK_DIR_MAX_AGE_HOURS="${WORK_DIR_MAX_AGE_HOURS:-24}" STATUS=0 -say() { echo "[janitor] $*"; } - -# "Is anything using this?" — ONE snapshot of every open path on the box -# (lsof -Fn), then a prefix match per candidate. `lsof +D ` walks the whole -# tree instead and took minutes on a 5 GB leftover — unusable from cron. An -# absent lsof means "cannot prove idle": the sweep keeps the path and says so. -LSOF_BIN="${JANITOR_LSOF:-lsof}" -have_busy_tools() { command -v "$LSOF_BIN" >/dev/null 2>&1; } -SNAP="" -cleanup() { [ -n "$SNAP" ] && rm -f -- "$SNAP"; } -trap cleanup EXIT -# One lsof for the whole run (~13 s / 83k lines on the box), kept ONLY for the -# bases we sweep — 460 candidates grepping a re-printed 83k-line string was the -# slow part, not lsof itself. -snapshot_open_paths() { - have_busy_tools || return 0 - SNAP=$(mktemp) || return 0 - local prefixes="" b - for b in $JANITOR_TMP_BASES $JANITOR_WORK_TEMP_BASES; do [ -d "$b" ] && prefixes="$prefixes"$'\n'"$b/"; done - # -F n: one "n" line per open file; -w: no warnings - "$LSOF_BIN" -w -Fn 2>/dev/null | sed -n 's/^n//p' | grep -F -f <(printf '%s' "$prefixes" | sed '/^$/d') > "$SNAP" 2>/dev/null || true -} -is_busy() { - local p="$1" - [ -n "$SNAP" ] && [ -s "$SNAP" ] || return 1 - # exact path, or anything beneath it when it is a directory - grep -qxF -- "$p" "$SNAP" && return 0 - [ -d "$p" ] && grep -qF -- "$p/" "$SNAP" -} - -# sweep : only names our tooling creates, never through -# a symlinked base, never across a filesystem, and remove+check in one step. -sweep() { - local base="$1" max_min="$2" p - [ -d "$base" ] || return 0 - [ -L "$base" ] && { say "skip symlinked base: $base"; return 0; } - while IFS= read -r -d '' p; do - if ! have_busy_tools; then say "cannot prove idle (lsof missing — apt install lsof), kept: $p"; STATUS=1; continue; fi - if is_busy "$p"; then say "busy, kept: $p"; continue; fi - if [ "$DRY_RUN" -eq 1 ]; then say "would remove ($(( max_min / 60 ))h+): $p"; else rm -rf -- "$p" && say "removed ($(( max_min / 60 ))h+): $p"; fi - done < <(find -P "$base" -xdev -mindepth 1 -maxdepth 1 \ - \( -name 'runner-*' -o -name 'omniroute-*' -o -name 'next-build*' -o -name 'e2e-build.tar.gz' \) \ - ! -type l -mmin "+$max_min" -print0 2>/dev/null || true) -} -say "$(date -u +%FT%TZ) start${DRY_RUN:+ (dry-run=$DRY_RUN)} busy-tools=$(have_busy_tools && echo ok || echo MISSING)" - -# 1) stale artefacts — tmpfs is RAM, so it gets the short fuse -snapshot_open_paths -for base in $JANITOR_TMP_BASES; do sweep "$base" $(( TMPFS_MAX_AGE_HOURS * 60 )); done -for base in $JANITOR_WORK_TEMP_BASES; do sweep "$base" $(( WORK_TEMP_MAX_AGE_HOURS * 60 )); done -say "stale temp sweep done" - -# 2) zombie builds -ZOMBIES=0 -while read -r pid etimes comm; do - [ -n "${pid:-}" ] || continue - if [ "$etimes" -gt $(( ZOMBIE_BUILD_MAX_MIN * 60 )) ]; then - say "⚠ zombie build pid=$pid comm=$comm age=$(( etimes / 60 ))min > ${ZOMBIE_BUILD_MAX_MIN}min — no job runs this long" - if [ "$DRY_RUN" -eq 1 ]; then say "[dry-run] would: kill -TERM $pid (then -KILL)"; else - kill -TERM "$pid" 2>/dev/null || true; sleep 10 - kill -0 "$pid" 2>/dev/null && { kill -KILL "$pid" 2>/dev/null || true; say " needed SIGKILL"; } - fi - ZOMBIES=$(( ZOMBIES + 1 )); STATUS=1 - fi -done < <(ps -eo pid=,etimes=,comm= 2>/dev/null | awk -v c="$ZOMBIE_BUILD_COMM" '$3 ~ ("^" c) {print $1, $2, $3}' || true) -say "zombie builds: $ZOMBIES" - -# 3) old checkouts of STOPPED runners -for d in $JANITOR_RUNNER_DIRS; do - [ -d "$d" ] && [ -f "$d/.runner" ] || continue - agent=$(grep -o '"agentName": *"[^"]*"' "$d/.runner" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/') - [ -n "$agent" ] || continue - unit=$(systemctl list-units --plain --no-legend "actions.runner.*.${agent}.service" 2>/dev/null | awk 'NR==1{print $1}') - [ -n "$unit" ] || continue - if systemctl is-active --quiet "$unit"; then continue; fi - while IFS= read -r -d '' co; do - if [ "$DRY_RUN" -eq 1 ]; then say "would prune checkout of stopped runner $agent: $co"; else rm -rf -- "$co" && say "pruned checkout of stopped runner $agent: $co"; fi - done < <(find -P "$d/_work" -xdev -mindepth 2 -maxdepth 2 -type d -mmin "+$(( WORK_CHECKOUT_MAX_AGE_HOURS * 60 ))" -print0 2>/dev/null || true) +echo "[janitor] $(date -u +%FT%TZ) start" + +# 1) Sweep stale runner temp/work leftovers (>24h — no legitimate job runs that long). +# Hardened for a root cron on world-writable paths: never follow a symlinked base +# (a compromised runner could plant one), -P + -xdev so the sweep cannot traverse +# out of the filesystem, and patterns narrowed to names OUR tooling creates +# (no generic tmp* — unrelated system temp files are out of scope). +for base in /tmp /home/*/actions-runner*/_work/_temp; do + [ -d "$base" ] || continue + [ -L "$base" ] && { echo "[janitor] skip symlinked base: $base"; continue; } + find -P "$base" -xdev -maxdepth 1 \( -name 'runner-*' -o -name 'omniroute-*' \) \ + ! -type l -mmin +$((WORK_DIR_MAX_AGE_HOURS * 60)) -exec rm -rf {} + 2>/dev/null || true done +echo "[janitor] stale temp sweep done" -# 4a) disk -USAGE=$(df --output=pcent "$JANITOR_DF_PATH" 2>/dev/null | tail -1 | tr -dc '0-9') -if [ "${USAGE:-0}" -ge "$DISK_ALERT_PCT" ]; then - say "⚠ ROOT DISK ${USAGE}% >= ${DISK_ALERT_PCT}% — clean before the next heavy run"; STATUS=1 +# 2) Disk pressure — alert loudly before SQLITE_FULL kills jobs mid-run. +USAGE=$(df --output=pcent / | tail -1 | tr -dc '0-9') +if [ "$USAGE" -ge "$DISK_ALERT_PCT" ]; then + echo "[janitor] ⚠ ROOT DISK ${USAGE}% >= ${DISK_ALERT_PCT}% — clean before the next heavy run" + STATUS=1 else - say "disk ${USAGE:-?}% OK" -fi - -# 4b) memory pressure (PSI) — the box swapped its way through the v3.8.50 publish -if [ -r "$JANITOR_PSI_FILE" ]; then - FULL60=$(awk '/^full/ {for(i=1;i<=NF;i++) if ($i ~ /^avg60=/) {sub("avg60=","",$i); print $i}}' "$JANITOR_PSI_FILE" 2>/dev/null || echo "") - if [ -n "$FULL60" ] && awk -v v="$FULL60" -v t="$PSI_FULL_AVG60_ALERT" 'BEGIN{exit !(v+0 >= t+0)}'; then - say "⚠ MEMORY PRESSURE psi full/avg60=${FULL60}% >= ${PSI_FULL_AVG60_ALERT}% — too many heavy jobs at once"; STATUS=1 - else - say "memory psi full/avg60=${FULL60:-n/a}% OK" - fi + echo "[janitor] disk ${USAGE}% OK" fi -# 4c) concurrency ceiling — alert with a breakdown; the fix is fewer/labelled -# runners (an operator decision), not killing listeners from cron. +# 3) Concurrency ceiling — 8-wide OOMed the 16 GB box twice on release day; +# 4 is the proven ceiling. This CODIFIES the rule that was manual discipline. ACTIVE=$(pgrep -fc "Runner.Listener" || true) -OMNI=$(pgrep -fc "actions-runner-omniroute[^ ]*/bin[^ ]*/Runner.Listener" || true) if [ "${ACTIVE:-0}" -gt "$MAX_ACTIVE_RUNNERS" ]; then - say "⚠ ${ACTIVE} Runner.Listener processes (omniroute=${OMNI:-0}, other=$(( ${ACTIVE:-0} - ${OMNI:-0} ))) > ceiling ${MAX_ACTIVE_RUNNERS} — stop idle extras: systemctl stop only when it has no Runner.Worker child" + echo "[janitor] ⚠ ${ACTIVE} Runner.Listener processes > ceiling ${MAX_ACTIVE_RUNNERS} — stop the extra runners (systemctl stop actions.runner.)" STATUS=1 else - say "runners active: ${ACTIVE:-0}/${MAX_ACTIVE_RUNNERS} (omniroute=${OMNI:-0}) OK" + echo "[janitor] runners active: ${ACTIVE:-0}/${MAX_ACTIVE_RUNNERS} OK" fi -say "done status=$STATUS" +echo "[janitor] done status=$STATUS" exit "$STATUS" diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index e32705e9906..9e621b78a8d 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -733,6 +733,7 @@ export default function CombosPage() { const [metrics, setMetrics] = useState({}); const [testResults, setTestResults] = useState(null); const [testingCombo, setTestingCombo] = useState(null); + const [removingErrorModels, setRemovingErrorModels] = useState(false); const { copied, copy } = useCopyToClipboard(); const notify = useNotificationStore(); const [proxyTargetCombo, setProxyTargetCombo] = useState(null); @@ -950,6 +951,78 @@ export default function CombosPage() { } }; + const handleRemoveErrorModels = async (errorStepIds: string[]) => { + if (!testingCombo || errorStepIds.length === 0) return; + const combo = combos.find((c) => c.name === testingCombo); + if (!combo) return; + setRemovingErrorModels(true); + try { + // Filter out models whose stepId matches an error result. + // stepId format is typically "free-manifest-model-N-provider/model" + // combo.models entries have an index-derived stepId; match via the + // model string that was used to generate the stepId. + const errorModels = new Set(errorStepIds); + const filteredModels = (combo.models || []).filter((_entry, index) => { + // Build the stepId the same way resolveNestedComboTargets would: + // "-model-<1-based-index>-" + const modelStr = typeof _entry === "string" ? _entry : _entry?.model || ""; + const stepId = `${combo.name}-model-${index + 1}-${modelStr}`; + return !errorModels.has(stepId); + }); + if (filteredModels.length === (combo.models || []).length) { + // Fallback: match by model string directly from testResults + const errorModelStrs = (testResults?.results || []) + .filter((r) => r.status === "error") + .map((r) => r.model) + .filter(Boolean); + const errorModelSet = new Set(errorModelStrs); + const fallbackFiltered = (combo.models || []).filter((entry) => { + const modelStr = typeof entry === "string" ? entry : entry?.model || ""; + return !errorModelSet.has(modelStr); + }); + if (fallbackFiltered.length < (combo.models || []).length) { + if (fallbackFiltered.length === 0) { + notify.error( + t.has?.("cannotRemoveAllModels") + ? t("cannotRemoveAllModels") + : "Cannot remove all models from combo" + ); + return; + } + await handleUpdate(combo.id, { + ...combo, + models: fallbackFiltered, + }); + setTestResults(null); + setTestingCombo(null); + notify.success(t("errorModelsRemoved")); + return; + } + notify.error(t("noErrorModelsFound")); + return; + } + if (filteredModels.length === 0) { + notify.error( + t.has?.("cannotRemoveAllModels") + ? t("cannotRemoveAllModels") + : "Cannot remove all models from combo" + ); + return; + } + await handleUpdate(combo.id, { + ...combo, + models: filteredModels, + }); + setTestResults(null); + setTestingCombo(null); + notify.success(t("errorModelsRemoved")); + } catch { + notify.error(t("failedUpdate")); + } finally { + setRemovingErrorModels(false); + } + }; + const handleToggleCombo = async (combo) => { const newActive = combo.isActive === false ? true : false; const previousActive = combo.isActive !== false; @@ -1316,7 +1389,11 @@ export default function CombosPage() { }} title={t("testResults", { name: testingCombo })} > - + )} @@ -1878,7 +1955,16 @@ function ComboCardInner({ } const ComboCard = memo(ComboCardInner); -function TestResultsView({ results }) { +function TestResultsView({ + results, + onRemoveErrorModels, + removingErrorModels, +}: { + results: any; + onRemoveErrorModels?: (errorStepIds: string[]) => void; + removingErrorModels?: boolean; +}) { + const t = useTranslations("combos"); const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible); if (results.error) { @@ -1890,6 +1976,9 @@ function TestResultsView({ results }) { ); } + const errorResults = (results.results || []).filter((r) => r.status === "error"); + const hasErrors = errorResults.length > 0; + return (
{results.resolvedBy && ( @@ -1900,65 +1989,102 @@ function TestResultsView({ results }) {
Resolved by:{" "} - + {results.resolvedBy}
- {results.resolvedByTarget?.connectionId || results.resolvedByTarget?.stepId ? ( + {results.resolvedByTarget?.connectionId || results.resolvedByTarget?.label ? (
- {results.resolvedByTarget?.connectionId - ? `account ${results.resolvedByTarget.connectionId.slice(0, 8)}` - : "dynamic account"} - {results.resolvedByTarget?.stepId - ? ` · step ${results.resolvedByTarget.stepId}` + {results.resolvedByTarget?.label + ? pickDisplayValue( + [results.resolvedByTarget.label], + emailsVisible, + results.resolvedByTarget.label + ) + : results.resolvedByTarget?.connectionId + ? `account ${results.resolvedByTarget.connectionId.slice(0, 8)}` + : "dynamic account"} + {results.resolvedByTarget?.connectionId && results.resolvedByTarget?.label + ? ` (acct ${results.resolvedByTarget.connectionId.slice(0, 8)})` : ""}
) : null}
)} - {results.results?.map((r, i) => ( -
- { + const displayLabel = r.label ? pickDisplayValue([r.label], emailsVisible, r.label) : null; + const accountStr = r.connectionId + ? `acct ${r.connectionId.slice(0, 8)}` + : "dynamic account"; + + return ( +
- {r.status === "ok" ? "check_circle" : r.status === "skipped" ? "skip_next" : "error"} - -
- - {pickDisplayValue([r.label], emailsVisible, r.model)} - - {r.connectionId || r.stepId ? ( -
- {r.connectionId ? `acct ${r.connectionId.slice(0, 8)}` : "dynamic account"} - {r.stepId ? ` · ${r.stepId}` : ""} + + {r.status === "ok" ? "check_circle" : r.status === "skipped" ? "skip_next" : "error"} + +
+
+ + {r.model} +
- ) : null} +
+ {displayLabel ? `${displayLabel} · ` : ""} + {accountStr} +
+
+ {r.latencyMs !== undefined && ( + {r.latencyMs}ms + )} + + {r.status} +
- {r.latencyMs !== undefined && {r.latencyMs}ms} - +
- ))} + )}
); } diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx index 1648fcd879d..9950f7b606a 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx @@ -130,7 +130,7 @@ export default function PassthroughModelsSection({ const [modelFilter, setModelFilter] = useState(""); const [testingAll, setTestingAll] = useState(false); const [testProgress, setTestProgress] = useState<{ done: number; total: number } | null>(null); - const [localAutoHideFailed, setLocalAutoHideFailed] = useState(false); + const [localAutoHideFailed, setLocalAutoHideFailed] = useState(true); const autoHideFailed = autoHideFailedProp !== undefined ? autoHideFailedProp : localAutoHideFailed; const setAutoHideFailed = onAutoHideFailedChange ?? setLocalAutoHideFailed; diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts index d49bbc989f3..922ac3de4ba 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts @@ -119,7 +119,7 @@ export function useModelVisibilityHandlers({ const [modelTestStatus, setModelTestStatus] = useState>({}); const [testingAll, setTestingAll] = useState(false); const [testProgress, setTestProgress] = useState<{ done: number; total: number } | null>(null); - const [autoHideFailed, setAutoHideFailed] = useState(false); + const [autoHideFailed, setAutoHideFailed] = useState(true); const [visibilityFilter, setVisibilityFilter] = useState<"all" | "visible" | "hidden">("all"); const providerAliasEntries = useMemo( diff --git a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx index 91e4ef82599..c0fa8851e6f 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx @@ -645,7 +645,11 @@ export default function SystemStorageTab() { await loadStorageHealth(); if (backupsExpanded) await loadBackups(); } else { - setImportStatus({ type: "error", message: data.error || t("importFailed") }); + const errorMsg = + typeof data?.error === "string" + ? data.error + : data?.error?.message || t("importFailed"); + setImportStatus({ type: "error", message: errorMsg }); } } catch { setImportStatus({ type: "error", message: t("errorDuringImport") }); @@ -699,13 +703,18 @@ export default function SystemStorageTab() { ? "bg-blue-500/10 text-blue-500 border border-blue-500/20" : "bg-red-500/10 text-red-500 border border-red-500/20"); + const displayMessage = + typeof status.message === "object" + ? status.message?.message || JSON.stringify(status.message) + : String(status.message); + return (
- {status.message} + {displayMessage}
); diff --git a/src/app/api/db-backups/import/route.ts b/src/app/api/db-backups/import/route.ts index 8186abe23bf..a2339492208 100644 --- a/src/app/api/db-backups/import/route.ts +++ b/src/app/api/db-backups/import/route.ts @@ -16,7 +16,7 @@ import { getSettings } from "@/lib/db/settings"; import { setSystemPromptConfig } from "@omniroute/open-sse/services/systemPrompt.ts"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; -const DEFAULT_MAX_UPLOAD_MB = 100; +const DEFAULT_MAX_UPLOAD_MB = 1024; // Hard ceiling so a misconfigured/hostile value can't ask the route to buffer an // unbounded file into memory. const MAX_UPLOAD_MB_CEILING = 4096; diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index f3cab6d48cb..46a22184557 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -3212,6 +3212,10 @@ "errorUpdating": "Error updating combo", "errorDeleting": "Error deleting combo", "testFailed": "Test request failed", + "removeErrorModels": "Delete {count} failed model(s)", + "removingErrorModels": "Removing…", + "errorModelsRemoved": "Failed models removed from combo", + "noErrorModelsFound": "No matching error models found", "failedToggle": "Failed to toggle combo", "testResults": "Test Results — {name}", "resolvedBy": "Resolved by:", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index ac3f4e21239..e3b864b3f5c 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -3212,6 +3212,10 @@ "errorUpdating": "更新组合时出错", "errorDeleting": "删除组合时出错", "testFailed": "测试请求失败", + "removeErrorModels": "删除 {count} 个失败模型", + "removingErrorModels": "正在删除…", + "errorModelsRemoved": "已从组合中移除失败模型", + "noErrorModelsFound": "未找到匹配的失败模型", "failedToggle": "切换组合状态失败", "testResults": "测试结果 — {name}", "resolvedBy": "最终由以下模型处理:", diff --git a/src/lib/db/migrations/163_model_capabilities.sql b/src/lib/db/migrations/163_model_capabilities.sql deleted file mode 100644 index bf81e76ae1a..00000000000 --- a/src/lib/db/migrations/163_model_capabilities.sql +++ /dev/null @@ -1,43 +0,0 @@ --- 163_model_capabilities.sql --- --- Promote `model_capabilities` from a lazily-created runtime table to a real migration. --- --- WHY: the table was only ever created by `ensureCapabilitiesTable()` in --- src/lib/modelsDevSync.ts, on demand, the first time a models.dev capability sync ran. --- Whether a database has it therefore depends on TIMING, not on the schema version — so a --- clean install and an upgraded install diverge for no structural reason. The v3.8.50 --- publish run hit exactly that: `check:install-upgrade` reported `model_capabilities` as a --- table "present only after upgrade", because the older database had already run a sync --- and the freshly-installed one had not. --- --- Creating it here makes both install paths converge deterministically. --- `ensureCapabilitiesTable()` stays in place as an idempotent safety net (it is a --- CREATE TABLE IF NOT EXISTS and now always a no-op); tests/unit/db-install-upgrade-schema-parity.test.ts --- pins the two definitions against drift. --- --- IF NOT EXISTS is required, not decorative: every database that ever ran a models.dev --- sync already has this table, and this migration must be a no-op there. - -CREATE TABLE IF NOT EXISTS model_capabilities ( - provider TEXT NOT NULL, - model_id TEXT NOT NULL, - tool_call BOOLEAN, - reasoning BOOLEAN, - attachment BOOLEAN, - structured_output BOOLEAN, - temperature BOOLEAN, - modalities_input TEXT, - modalities_output TEXT, - knowledge_cutoff TEXT, - release_date TEXT, - last_updated TEXT, - status TEXT, - family TEXT, - open_weights BOOLEAN, - limit_context INTEGER, - limit_input INTEGER, - limit_output INTEGER, - interleaved_field TEXT, - last_synced TEXT, - PRIMARY KEY (provider, model_id) -); diff --git a/tests/unit/alibaba-free-tier-allowlist.test.ts b/tests/unit/alibaba-free-tier-allowlist.test.ts index df468d82e53..1fa87dc5e1a 100644 --- a/tests/unit/alibaba-free-tier-allowlist.test.ts +++ b/tests/unit/alibaba-free-tier-allowlist.test.ts @@ -7,9 +7,6 @@ */ import { test } from "node:test"; import assert from "node:assert/strict"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { ALIBABA_FREE_TIER_TEXT_CAPABLE_MODELS, ALIBABA_NO_FREE_TIER_TEXT_MODELS, @@ -30,90 +27,18 @@ test("built-in allowlist includes operator free models and excludes paid blockli assert.equal(isAlibabaBuiltinFreeTierTextModel("qwen3.7-max"), false); }); -/** - * The shipped `config/alibaba-free-tier-allowlist.json` carries a `validUntil`, - * so asserting against it made this test a time bomb: it went red on its own on - * 2026-08-28, the day after the pack expired, and stayed red on every PR and on - * `main` (#11866). Nothing had changed — the clock moved. - * - * Production was never affected: an expired pack falls back to the embedded - * list by design. So the contract worth pinning is the BEHAVIOR on both sides of - * the expiry, with packs this test owns and dates it controls — never the - * freshness of the catalog that ships in the repo. - */ -function withAllowlistPack( - pack: Record, - assertions: () => void -): void { - const dir = mkdtempSync(join(tmpdir(), "alibaba-allowlist-")); - const packPath = join(dir, "allowlist.json"); - writeFileSync(packPath, JSON.stringify(pack), "utf8"); - +test("allowlist JSON pack overrides embedded lists when valid", () => { const previousPath = process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH; + const packPath = `${process.cwd()}/config/alibaba-free-tier-allowlist.json`; process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH = packPath; resetAlibabaFreeTierAllowlistCache(); - try { - assertions(); - } finally { - if (previousPath) process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH = previousPath; - else delete process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH; - resetAlibabaFreeTierAllowlistCache(); - rmSync(dir, { recursive: true, force: true }); - } -} -test("allowlist JSON pack overrides embedded lists while it is still valid", () => { - withAllowlistPack( - { - asOf: "2026-07-28", - validUntil: "2999-01-01", - capable: ["pack-only-capable-model", "qwen3.6-plus"], - noFreeTier: ["pack-only-paid-model"], - }, - () => { - const pack = loadAlibabaFreeTierAllowlistPack(); - assert.ok(pack, "a pack inside its validity window must load"); - assert.ok(isAlibabaFreeTierAllowlistPackValid(pack!)); - assert.ok(pack!.capable.includes("qwen3.6-plus")); - // Positive anchor: the pack must actually REPLACE the embedded list, not - // merely load. `pack-only-capable-model` exists nowhere else. - assert.equal(isAlibabaBuiltinFreeTierTextModel("pack-only-capable-model"), true); - assert.equal(isAlibabaBuiltinNoFreeTierTextModel("pack-only-paid-model"), true); - } - ); -}); - -test("an expired allowlist pack is ignored and the embedded list serves instead", () => { - // This is the path production has actually been on since 2026-08-27, and it - // had no coverage at all — which is why the expiry surfaced as a red test - // rather than as a deliberate, understood fallback. - withAllowlistPack( - { - asOf: "2026-07-28", - validUntil: "2026-08-27", - capable: ["pack-only-capable-model"], - noFreeTier: ["pack-only-paid-model"], - }, - () => { - assert.equal(loadAlibabaFreeTierAllowlistPack(), null, "expired pack must not load"); - assert.equal(isAlibabaBuiltinFreeTierTextModel("pack-only-capable-model"), false); - // The embedded list must be what answers once the pack is rejected. - assert.equal(isAlibabaBuiltinFreeTierTextModel("qwen3.6-plus"), true); - assert.equal(isAlibabaBuiltinNoFreeTierTextModel("qwen3.7-max"), true); - } - ); -}); + const pack = loadAlibabaFreeTierAllowlistPack(); + assert.ok(pack); + assert.ok(isAlibabaFreeTierAllowlistPackValid(pack!)); + assert.ok(pack!.capable.includes("qwen3.6-plus")); -test("isAlibabaFreeTierAllowlistPackValid compares against the instant it is given", () => { - const pack = { asOf: "2026-07-28", validUntil: "2026-08-27", capable: ["x"], noFreeTier: [] }; - assert.equal(isAlibabaFreeTierAllowlistPackValid(pack, Date.parse("2026-08-26")), true); - assert.equal(isAlibabaFreeTierAllowlistPackValid(pack, Date.parse("2026-08-28")), false); - // No expiry declared means the pack never goes stale on its own. - assert.equal( - isAlibabaFreeTierAllowlistPackValid( - { asOf: "2026-07-28", capable: ["x"], noFreeTier: [] }, - Date.parse("2999-01-01") - ), - true - ); + if (previousPath) process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH = previousPath; + else delete process.env.ALIBABA_FREE_TIER_ALLOWLIST_PATH; + resetAlibabaFreeTierAllowlistCache(); }); diff --git a/tests/unit/check-install-upgrade-convergence.test.ts b/tests/unit/check-install-upgrade-convergence.test.ts index 47f8c510879..de3b74716a2 100644 --- a/tests/unit/check-install-upgrade-convergence.test.ts +++ b/tests/unit/check-install-upgrade-convergence.test.ts @@ -2,10 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; // @ts-expect-error — plain .mjs gate script, no type declarations by design -import { - assertNoDiskExhaustion, - evaluateConvergence, -} from "../../scripts/check/check-install-upgrade.mjs"; +import { evaluateConvergence } from "../../scripts/check/check-install-upgrade.mjs"; /** * The whole point of this gate is that the two directions of schema divergence are NOT @@ -69,11 +66,7 @@ test("UNKNOWN residue fails — a new divergence must not hide behind the allowl assert.equal(v.ok, false); assert.deepEqual(v.unknownResidue, ["surprise_table"]); assert.match(v.failures[0], /surprise_table/); - assert.doesNotMatch( - v.failures[0], - /cache_metrics/, - "the known one must not be re-reported as new" - ); + assert.doesNotMatch(v.failures[0], /cache_metrics/, "the known one must not be re-reported as new"); }); test("both directions at once report both failures", () => { @@ -100,43 +93,3 @@ test("empty/missing inputs do not crash", () => { assert.equal(v.ok, true); assert.deepEqual(v.onlyFresh, []); }); - -// ─── ENOSPC guard ────────────────────────────────────────────────────────────── -// -// The v3.8.50 publish run (CI 33104507735) failed with "15 tables a CLEAN install creates -// but an UPGRADE does not". None of them was missing: the Phase B upgrade install had hit -// `npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device` 5611 times, npm still -// exited 0, the truncated `omniroute serve` "exited with code 0 before serving", and the -// database therefore still held the 3.8.49 schema. npm reporting disk exhaustion as a -// warning is what let a full disk masquerade as a schema defect. - -test("npm ENOSPC warnings are raised as an install failure, not ignored", () => { - const enospc = "npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device, write\n".repeat(3); - assert.throws( - () => assertNoDiskExhaustion(enospc, "upgrade install"), - (err: Error) => { - assert.match(err.message, /upgrade install/); - assert.match(err.message, /ran out of disk space/); - assert.match(err.message, /3 ENOSPC error/); - // The operator must not go looking for a migration that is not missing. - assert.match(err.message, /NOT a schema divergence/); - return true; - } - ); -}); - -test("a clean install log does not trip the disk guard", () => { - assert.doesNotThrow(() => - assertNoDiskExhaustion( - "npm warn deprecated boolean@3.2.0: Package no longer supported.\nadded 900 packages\n", - "clean install" - ) - ); -}); - -test("the guard also catches the bare kernel message without the ENOSPC code", () => { - assert.throws( - () => assertNoDiskExhaustion("Error: no space left on device", "clean install"), - /ran out of disk space/ - ); -}); diff --git a/tests/unit/check-workflows-provenance-runner.test.ts b/tests/unit/check-workflows-provenance-runner.test.ts deleted file mode 100644 index d7edb51cece..00000000000 --- a/tests/unit/check-workflows-provenance-runner.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync, readdirSync } from "node:fs"; -import { join } from "node:path"; - -import { - classifyRunsOn, - findProvenanceOnSelfHosted, -} from "../../scripts/check/lib/provenanceRunner.mjs"; - -/** - * v3.8.50, 10th publish attempt, 76 minutes in — after the tag, the GitHub - * Release and the Docker images were already public: - * - * 422 Unprocessable Entity - Error verifying sigstore provenance bundle: - * Unsupported GitHub Actions runner environment: "self-hosted". - * - * `USE_VPS_RUNNER` had routed the publish job to the .113 pool on 2026-08-02; - * no release happened between 07-30 and 08-28, so nothing surfaced it. The - * pairing is pure text, so it must fail the workflow lint on the PR that - * introduces it. - */ -const ROOT = join(import.meta.dirname, "../.."); -const WORKFLOWS = join(ROOT, ".github/workflows"); - -// The exact runs-on expression npm-publish.yml used when it broke. -const VPS_EXPR = - "${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('[\"self-hosted\",\"omni-release\"]') || 'ubuntu-latest' }}"; - -function workflow(runsOn: string, run: string, extra = ""): string { - return [ - "name: t", - "on: push", - "jobs:", - " publish:", - ` runs-on: ${runsOn}`, - extra, - " steps:", - " - name: upload", - ` run: ${run}`, - "", - ].join("\n"); -} - -test("classifyRunsOn: literal, array, object-with-labels and the fromJSON expression are self-hosted", () => { - assert.equal(classifyRunsOn("self-hosted"), "self-hosted"); - assert.equal(classifyRunsOn(["self-hosted", "omni-release"]), "self-hosted"); - assert.equal(classifyRunsOn({ group: "Default", labels: ["self-hosted"] }), "self-hosted"); - assert.equal(classifyRunsOn(VPS_EXPR), "self-hosted"); -}); - -test("classifyRunsOn: hosted labels are hosted, opaque expressions are unknown (never guessed)", () => { - assert.equal(classifyRunsOn("ubuntu-latest"), "hosted"); - assert.equal(classifyRunsOn(["ubuntu-latest"]), "hosted"); - assert.equal(classifyRunsOn("${{ matrix.os }}"), "unknown"); - assert.equal(classifyRunsOn(undefined), "unknown"); -}); - -test("flags --provenance inside a job routed to the self-hosted pool", () => { - const found = findProvenanceOnSelfHosted( - workflow( - JSON.stringify(VPS_EXPR), - 'npm stage publish --provenance --access public --tag "$TAG"' - ), - "npm-publish.yml" - ); - assert.deepEqual(found, [{ file: "npm-publish.yml", job: "publish", step: "upload" }]); -}); - -test("also catches the literal label and the --provenance-file form", () => { - assert.equal( - findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --provenance")).length, - 1 - ); - assert.equal( - findProvenanceOnSelfHosted( - workflow("[self-hosted, omni-release]", "npm publish --provenance-file=./p.json") - ).length, - 0, - "--provenance-file is a different flag (a pre-built bundle) and is not what the registry rejects" - ); - assert.equal( - findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --provenance=true")).length, - 1 - ); -}); - -test("does not flag hosted jobs, unknown runners, or self-hosted jobs without the flag", () => { - assert.deepEqual( - findProvenanceOnSelfHosted(workflow("ubuntu-latest", "npm publish --provenance")), - [] - ); - assert.deepEqual( - findProvenanceOnSelfHosted(workflow("${{ matrix.os }}", "npm publish --provenance")), - [] - ); - assert.deepEqual( - findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --access public")), - [] - ); - // The word only in a step NAME or a comment is not a finding. - assert.deepEqual( - findProvenanceOnSelfHosted( - [ - "name: t", - "on: push", - "jobs:", - " j:", - " runs-on: self-hosted", - " steps:", - " - name: provenance note", - " run: echo hi # --provenance later", - "", - ].join("\n") - ), - [], - "a comment after the command is still part of the run string — accept that the regex is conservative" - ); -}); - -test("reusable-workflow jobs (uses:) and unparseable YAML are not this rule's findings", () => { - const reusable = [ - "name: t", - "on: push", - "jobs:", - " j:", - " uses: ./.github/workflows/x.yml", - "", - ].join("\n"); - assert.deepEqual(findProvenanceOnSelfHosted(reusable), []); - assert.deepEqual(findProvenanceOnSelfHosted("jobs: [unclosed"), []); -}); - -test("regression guard: no workflow in this repo publishes with --provenance from a self-hosted runner", () => { - const files = readdirSync(WORKFLOWS).filter((f) => /\.ya?ml$/.test(f)); - assert.ok(files.length > 10, "expected the real workflow set"); - const findings = files.flatMap((f) => - findProvenanceOnSelfHosted(readFileSync(join(WORKFLOWS, f), "utf8"), f) - ); - assert.deepEqual( - findings, - [], - `npm rejects provenance from self-hosted runners (422) — move the upload to a github-hosted job: ${JSON.stringify(findings)}` - ); -}); diff --git a/tests/unit/config-expiry-time-bomb.test.ts b/tests/unit/config-expiry-time-bomb.test.ts deleted file mode 100644 index 7740e09e5c5..00000000000 --- a/tests/unit/config-expiry-time-bomb.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { - classifyExpiry, - collectExpiryFields, - scanConfigExpiry, -} from "../../scripts/check/lib/configExpiry.mjs"; - -/** - * Time bombs: config packs with a `validUntil` (or sibling key) that lapse with - * no commit involved. The Alibaba free-tier pack expired on 2026-08-27 and from - * the 28th every PR and main carried a red Unit Tests shard (#11866). Nothing a - * diff review could have caught. - * - * This suite fails SEVEN DAYS BEFORE any pack under config/ lapses, naming the - * file and key, so renewal happens on someone's terms instead of the clock's. - */ -const ROOT = join(import.meta.dirname, "../.."); -const CONFIG_DIR = join(ROOT, "config"); -const DAY = 86_400_000; -const WARN_DAYS = 7; - -/** - * Packs known to be expired/expiring, each pinned to the issue that owns the - * renewal decision. An entry whose pack is no longer expiring FAILS below as a - * stale allowlist entry — remove it when the pack is renewed. - */ -const ALLOWLIST: Record = { - "alibaba-free-tier-allowlist.json": - "#11866 — validUntil 2026-08-27 has passed; the loader already falls back to the embedded list, and renewing the curated free-tier pack is an operator data decision, not a test fix", -}; - -const NOW = Date.UTC(2026, 7, 28); // 2026-08-28, fixed: this suite must not itself depend on the clock -const day = (offset: number) => new Date(NOW + offset * DAY).toISOString().slice(0, 10); - -test("collectExpiryFields: finds nested and array-nested expiry keys, ignores non-string values", () => { - const fields = collectExpiryFields( - { - validUntil: day(3), - nested: { expiresAt: day(30), other: "x" }, - list: [{ expiry: day(-1) }, { expires: 12345 }], - expires_at: "not a date", - }, - "pack.json" - ); - assert.deepEqual( - fields.map((f) => [f.keyPath, f.expiresAt === null ? null : "date"]), - [ - ["validUntil", "date"], - ["nested.expiresAt", "date"], - ["list.0.expiry", "date"], - ["expires_at", null], - ] - ); -}); - -test("classifyExpiry: expired / expiring inside the warning window / ok / unparseable", () => { - const f = (raw: string) => ({ - file: "p", - keyPath: "validUntil", - raw, - expiresAt: Number.isFinite(Date.parse(raw)) ? Date.parse(raw) : null, - }); - assert.equal(classifyExpiry(f(day(-1)), NOW, WARN_DAYS), "expired"); - assert.equal( - classifyExpiry(f(day(0)), NOW, WARN_DAYS), - "expiring", - "lapsing today is already too late to be 'ok'" - ); - assert.equal(classifyExpiry(f(day(6)), NOW, WARN_DAYS), "expiring"); - assert.equal(classifyExpiry(f(day(8)), NOW, WARN_DAYS), "ok"); - assert.equal(classifyExpiry(f("never"), NOW, WARN_DAYS), "unparseable"); -}); - -test("scanConfigExpiry: walks a config tree, skips node_modules and invalid JSON, keys files portably", () => { - const dir = mkdtempSync(join(tmpdir(), "cfg-expiry-")); - try { - mkdirSync(join(dir, "sub"), { recursive: true }); - mkdirSync(join(dir, "node_modules", "dep"), { recursive: true }); - writeFileSync(join(dir, "a.json"), JSON.stringify({ validUntil: day(3) })); - writeFileSync(join(dir, "sub", "b.json"), JSON.stringify({ deep: { expiresAt: day(40) } })); - writeFileSync( - join(dir, "node_modules", "dep", "c.json"), - JSON.stringify({ validUntil: day(-5) }) - ); - writeFileSync(join(dir, "broken.json"), "{ not json"); - writeFileSync(join(dir, "notes.txt"), JSON.stringify({ validUntil: day(-5) })); - const found = scanConfigExpiry(dir).map((f) => `${f.file}:${f.keyPath}`); - assert.deepEqual(found, ["a.json:validUntil", "sub/b.json:deep.expiresAt"]); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -}); - -test(`repo: no pack under config/ lapses within ${WARN_DAYS} days unless its renewal is tracked`, (t) => { - const fields = scanConfigExpiry(CONFIG_DIR); - // Positive anchor: the scanner must be seeing SOMETHING, or a renamed key - // would silently turn this whole suite into a no-op. - assert.ok( - fields.length >= 1, - "expected at least one dated pack under config/ (the Alibaba allowlist) — if the key was renamed, extend EXPIRY_KEY" - ); - - const failures: string[] = []; - const seenAllowlisted = new Set(); - for (const f of fields) { - const status = classifyExpiry(f, Date.now(), WARN_DAYS); - const tracked = ALLOWLIST[f.file]; - if (status === "unparseable") { - t.diagnostic(`${f.file} ${f.keyPath}="${f.raw}" is not a date — not monitored`); - continue; - } - if (status === "ok") continue; - if (tracked) { - seenAllowlisted.add(f.file); - t.diagnostic(`${f.file} ${f.keyPath}=${f.raw} is ${status} — tracked: ${tracked}`); - continue; - } - failures.push( - `${f.file} → ${f.keyPath}=${f.raw} is ${status}: renew the pack (or track it in ALLOWLIST with its issue)` - ); - } - for (const file of Object.keys(ALLOWLIST)) { - if (!seenAllowlisted.has(file)) { - failures.push( - `stale ALLOWLIST entry: ${file} is no longer expired/expiring — remove it (${ALLOWLIST[file]})` - ); - } - } - assert.deepEqual(failures, [], failures.join("\n")); -}); diff --git a/tests/unit/db-install-upgrade-schema-parity.test.ts b/tests/unit/db-install-upgrade-schema-parity.test.ts deleted file mode 100644 index 9ce6c408c48..00000000000 --- a/tests/unit/db-install-upgrade-schema-parity.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect): -// This test opens a real SQLite database through `src/lib/db/core.ts`. better-sqlite3 is a -// native addon; production and CI load it normally, but some sandboxes ship a system glibc -// older than the prebuilt binary requires ("GLIBC_2.29 not found"), in which case the -// runtime cascades to node:sqlite/sql.js. See tests/unit/_helpers/betterSqlite3Availability.ts. -// -// WHY THIS FILE EXISTS -// -------------------- -// `npm run check:install-upgrade` (scripts/check/check-install-upgrade.mjs) proves that a -// CLEAN install and an UPGRADE converge on the same schema, but it costs a full `npm pack` -// plus three global installs and three boots (~17 min in CI) and it can only ever run at -// publish time, against an already-published previous version. It is not a development -// feedback loop, and the v3.8.50 publish run is what proved it: the gate reported 15 -// "missing" tables, and the deterministic half of that verdict was never checkable locally. -// -// This file pins the deterministic half in milliseconds: -// -// 1. Every migration file on disk is actually reachable by the runner on a fresh install. -// A file the runner never applies is a table no user ever gets. -// 2. `model_capabilities` is created by the MIGRATION SET, not lazily at runtime. -// A table created on demand by `CREATE TABLE IF NOT EXISTS` inside a feature code -// path exists or not depending on whether that feature happened to run before the -// snapshot — so it diverges between the two install paths by TIMING, not by schema. -// That is precisely how `model_capabilities` surfaced as a divergence on the v3.8.50 -// publish run (present in the upgraded database, absent from the clean one). -import test from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); -const MIGRATIONS_DIR = path.join(REPO_ROOT, "src", "lib", "db", "migrations"); - -const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-install-upgrade-parity-")); -process.env.DATA_DIR = TEST_DATA_DIR; -process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; - -const core = await import("../../src/lib/db/core.ts"); - -// A clean install: core.ts applies the inline SCHEMA_SQL, the `ensure*Columns()` helpers, -// then runMigrations(). This is the exact code path Phase A of the gate exercises. -const db = core.getDbInstance(); - -test.after(() => { - try { - core.resetDbInstance(); - } catch { - /* best effort */ - } - fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); -}); - -function migrationFiles(): Array<{ version: string; name: string }> { - return fs - .readdirSync(MIGRATIONS_DIR) - .sort() - .map((file) => /^(\d{3,})_(.+)\.sql$/.exec(file)) - .filter((m): m is RegExpExecArray => m !== null) - .map((m) => ({ version: m[1], name: m[2] })); -} - -function hasTable(name: string): boolean { - return Boolean( - db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name) - ); -} - -test("a clean install applies every migration file on disk", () => { - const ledger = new Set( - ( - db.prepare("SELECT version FROM _omniroute_migrations").all() as Array<{ version: string }> - ).map((row) => row.version) - ); - const unapplied = migrationFiles() - .filter((m) => !ledger.has(m.version)) - .map((m) => `${m.version}_${m.name}`); - assert.deepEqual( - unapplied, - [], - "migration files the runner never applied — an upgrade would not create their tables either" - ); -}); - -test("model_capabilities comes from the migration set, not from a lazy runtime CREATE", () => { - assert.ok( - hasTable("model_capabilities"), - "model_capabilities must be created by a migration so a clean install and an upgrade " + - "converge deterministically instead of depending on whether the models.dev sync ran" - ); -}); - -test("the model_capabilities migration does not drift from ensureCapabilitiesTable()", () => { - const source = fs.readFileSync(path.join(REPO_ROOT, "src", "lib", "modelsDevSync.ts"), "utf8"); - const ddl = /CREATE TABLE IF NOT EXISTS model_capabilities\s*\(([\s\S]*?)\n\s*\)/.exec(source); - assert.ok(ddl, "ensureCapabilitiesTable() DDL not found in src/lib/modelsDevSync.ts"); - - const runtimeColumns = ddl[1] - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0 && !/^PRIMARY KEY/i.test(line)) - .map((line) => line.replace(/,$/, "").split(/\s+/)[0]) - .sort(); - - const migrationColumns = ( - db.prepare("PRAGMA table_info(model_capabilities)").all() as Array<{ name: string }> - ) - .map((column) => column.name) - .sort(); - - assert.deepEqual( - migrationColumns, - runtimeColumns, - "the migration and the runtime helper must create the same columns — a drift here means " + - "an upgraded database keeps the old shape while a clean install gets the new one" - ); -}); diff --git a/tests/unit/npm-payload-nft-manifests-excluded.test.ts b/tests/unit/npm-payload-nft-manifests-excluded.test.ts deleted file mode 100644 index 4ab0ff09365..00000000000 --- a/tests/unit/npm-payload-nft-manifests-excluded.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync, readdirSync } from "node:fs"; -import type { Dirent } from "node:fs"; -import { join } from "node:path"; - -/** - * v3.8.50 was refused by the registry with `413 Payload Too Large` on - * `POST /-/stage/package/omniroute`: the tarball had reached 288.7 MB packed - * (1.1 GB unpacked), against 174.5 MB for the 3.8.49 that published fine. - * - * 668.7 MB of that — 61% of the whole package — was 842 `*.nft.json` files. - * Those are Next.js Node File Trace manifests: build-time metadata used to - * COMPUTE the standalone bundle, never read while serving. They had doubled - * since 3.8.49 (325.0 MB across 748 files), which is what tipped the payload - * over the limit. - * - * The guard is the `files[]` negation, so a future entry that re-widens the - * glob (or a rewrite of the array) cannot silently put them back. - */ -const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "../../package.json"), "utf8")) as { - files?: string[]; -}; - -test("package.json files[] excludes Next's .nft.json trace manifests", () => { - const files = pkg.files ?? []; - assert.ok(files.length > 0, "package.json must declare files[]"); - assert.ok( - files.includes("!**/*.nft.json"), - "files[] must negate **/*.nft.json — they are build metadata and were 61% of the 3.8.50 payload" - ); -}); - -test("the negation sits after the positive dist/ entry it has to override", () => { - // npm applies files[] in order: a negation listed BEFORE the directory that - // pulls the files in is a no-op. Positive anchor, so this test cannot pass - // just because both strings happen to be present somewhere. - const files = pkg.files ?? []; - const dist = files.indexOf("dist/"); - const negation = files.indexOf("!**/*.nft.json"); - assert.notEqual(dist, -1, "dist/ must still be published"); - assert.ok(negation > dist, "the .nft.json negation must come after dist/"); -}); - -test("no source module reads a .nft.json at runtime", () => { - // If this ever stops holding, the exclusion above becomes a runtime break - // rather than a size win — which is exactly the assumption worth pinning. - const roots = ["src", "open-sse", "bin"]; - const hits: string[] = []; - for (const root of roots) { - const dir = join(import.meta.dirname, "../..", root); - const stack = [dir]; - while (stack.length > 0) { - const current = stack.pop() as string; - let entries: Dirent[]; - try { - entries = readdirSync(current, { withFileTypes: true }); - } catch { - continue; - } - for (const entry of entries) { - const full = join(current, entry.name); - if (entry.isDirectory()) { - if (entry.name !== "node_modules") stack.push(full); - } else if (/\.(ts|tsx|mjs|js)$/.test(entry.name)) { - if (readFileSync(full, "utf8").includes(".nft.json")) hits.push(full); - } - } - } - } - assert.deepEqual(hits, [], `nothing may depend on .nft.json at runtime: ${hits.join(", ")}`); -}); diff --git a/tests/unit/runner-janitor.test.ts b/tests/unit/runner-janitor.test.ts deleted file mode 100644 index 9519414fbc9..00000000000 --- a/tests/unit/runner-janitor.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - statSync, - utimesSync, - writeFileSync, -} from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -/** - * scripts/ops/runner-janitor.sh runs from cron on the .113 runner box. This - * suite pins its safety contract against a fixture tree — never the real /tmp: - * every base, the runner dirs, the PSI file and the df path are redirected, the - * zombie pattern is set to a name no process has, and the ceilings are lifted - * so the outcome does not depend on the box the test happens to run on. - */ -const ROOT = path.resolve(import.meta.dirname, "..", ".."); -const SCRIPT = path.join(ROOT, "scripts", "ops", "runner-janitor.sh"); -const HOUR = 3_600_000; -// The sweep needs lsof to PROVE a path is idle (one snapshot of open paths). Hosted CI -// images ship both; a bare devbox may not. Each branch below asserts what must -// hold in that environment — without the tools the contract is "delete nothing, -// say why", which is exactly the behaviour worth pinning. -const HAVE_BUSY_TOOLS = - spawnSync("bash", ["-c", "command -v lsof"], { stdio: "ignore" }).status === 0; - -function fixture() { - const base = mkdtempSync(path.join(os.tmpdir(), "janitor-fixture-")); - const old = new Date(Date.now() - 5 * HOUR); - const mk = (name: string, dir: boolean, when: Date | null) => { - const p = path.join(base, name); - if (dir) { - mkdirSync(p); - writeFileSync(path.join(p, "x"), "x"); - } else writeFileSync(p, "x"); - if (when) utimesSync(p, when, when); - return p; - }; - return { - base, - staleTar: mk("e2e-build.tar.gz", false, old), // fixed-name artefact ci.yml/npm-publish leave behind - staleBuild: mk("next-build-abc", true, old), - staleUpgrade: mk("omniroute-install-upgrade-xyz", true, old), - fresh: mk("omniroute-batch-api-fresh", true, null), // in use right now - unrelated: mk("somebody-elses.log", false, old), // not ours — never touched - }; -} - -function run(args: string[], base: string, extraEnv: Record = {}) { - return spawnSync("bash", [SCRIPT, ...args], { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - env: { - ...process.env, - JANITOR_TMP_BASES: base, - JANITOR_WORK_TEMP_BASES: "", - JANITOR_RUNNER_DIRS: path.join(base, "no-runners-here-*"), - JANITOR_PSI_FILE: path.join(base, "no-psi"), - JANITOR_DF_PATH: base, - ZOMBIE_BUILD_COMM: "janitor-test-no-such-process", - MAX_ACTIVE_RUNNERS: "9999", - DISK_ALERT_PCT: "101", - ...extraEnv, - }, - }); -} - -describe("runner-janitor.sh", () => { - it("is executable bash with strict mode and prints usage on --help", () => { - assert.ok(existsSync(SCRIPT)); - assert.ok(statSync(SCRIPT).mode & 0o111, "must be chmod +x (cron runs it directly)"); - const body = readFileSync(SCRIPT, "utf8"); - assert.ok(body.startsWith("#!/usr/bin/env bash")); - assert.ok(body.includes("set -euo pipefail")); - const help = run(["--help"], os.tmpdir()); - assert.equal(help.status, 0, help.stderr); - assert.match(help.stdout, /--dry-run/); - }); - - it("without lsof it cannot prove idle, so it deletes nothing and says why (exit 1)", () => { - const f = fixture(); - try { - const r = run([], f.base, { JANITOR_LSOF: "/nonexistent/lsof" }); - assert.equal(r.status, 1, "a janitor that cannot do its job must show up in the cron log"); - assert.match(r.stdout, /busy-tools=MISSING/); - assert.match( - r.stdout, - /cannot prove idle \(lsof missing — apt install lsof\), kept: .*e2e-build\.tar\.gz/ - ); - for (const p of [f.staleTar, f.staleBuild, f.staleUpgrade, f.fresh, f.unrelated]) { - assert.ok(existsSync(p), `must not delete ${p} when idleness cannot be proven`); - } - } finally { - rmSync(f.base, { recursive: true, force: true }); - } - }); - - it("--dry-run names what it WOULD remove and removes nothing", (t) => { - if (!HAVE_BUSY_TOOLS) return t.skip("lsof absent on this box — sweep branch covered in CI"); - const f = fixture(); - try { - const r = run(["--dry-run"], f.base); - assert.equal(r.status, 0, r.stderr + r.stdout); - assert.match(r.stdout, /busy-tools=ok/); - for (const p of [f.staleTar, f.staleBuild, f.staleUpgrade]) { - assert.match( - r.stdout, - new RegExp(`would remove \\(3h\\+\\): ${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`) - ); - assert.doesNotMatch( - r.stdout, - new RegExp(`removed \\(3h\\+\\): ${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), - "dry-run must never claim it removed something" - ); - assert.ok(existsSync(p), `dry-run must not delete ${p}`); - } - assert.doesNotMatch( - r.stdout, - /omniroute-batch-api-fresh/, - "a fresh dir is never a candidate" - ); - assert.doesNotMatch(r.stdout, /somebody-elses\.log/, "only names our tooling creates"); - assert.match(r.stdout, /zombie builds: 0/); - assert.match(r.stdout, /done status=0/); - } finally { - rmSync(f.base, { recursive: true, force: true }); - } - }); - - it("for real: sweeps the three stale artefacts, keeps the fresh one and the stranger", (t) => { - if (!HAVE_BUSY_TOOLS) return t.skip("lsof absent on this box — sweep branch covered in CI"); - const f = fixture(); - try { - const r = run([], f.base); - assert.equal(r.status, 0, r.stderr + r.stdout); - assert.ok(!existsSync(f.staleTar), "stale e2e-build.tar.gz must go (it is RAM on tmpfs)"); - assert.ok(!existsSync(f.staleBuild), "stale next-build dir must go"); - assert.ok(!existsSync(f.staleUpgrade), "stale install-upgrade dir must go"); - assert.ok(existsSync(f.fresh), "a fresh dir must survive"); - assert.ok(existsSync(f.unrelated), "files we did not create must survive even when old"); - } finally { - rmSync(f.base, { recursive: true, force: true }); - } - }); - - it("tmpfs fuse is shorter than the disk fuse (RAM vs disk), both overridable", () => { - const f = fixture(); - try { - // With a 6h tmpfs fuse the 5h-old artefacts are NOT stale yet. - const r = run(["--dry-run"], f.base, { TMPFS_MAX_AGE_HOURS: "6" }); - assert.doesNotMatch( - r.stdout, - /would remove|removed \(|cannot prove idle/, - "nothing is stale under a 6h fuse, so no candidate is even examined" - ); - const body = readFileSync(SCRIPT, "utf8"); - assert.match(body, /TMPFS_MAX_AGE_HOURS:-3\}/, "tmpfs default must stay short — it is RAM"); - assert.match(body, /WORK_TEMP_MAX_AGE_HOURS:-24\}/); - } finally { - rmSync(f.base, { recursive: true, force: true }); - } - }); - - it("alerts (exit 1) on disk and memory pressure thresholds without touching files", () => { - const f = fixture(); - try { - writeFileSync( - path.join(f.base, "psi"), - "some avg10=0.00 avg60=0.00 avg300=0.00 total=1\nfull avg10=0.00 avg60=23.50 avg300=9.00 total=1\n" - ); - const r = run(["--dry-run"], f.base, { - JANITOR_PSI_FILE: path.join(f.base, "psi"), - DISK_ALERT_PCT: "0", - }); - assert.equal(r.status, 1, "attention needed must be exit 1 for the cron log"); - assert.match(r.stdout, /MEMORY PRESSURE psi full\/avg60=23\.50%/); - assert.ok(existsSync(f.fresh) && existsSync(f.unrelated)); - assert.match(r.stdout, /ROOT DISK \d+% >= 0%/); - assert.ok(existsSync(f.staleTar), "alerting never deletes"); - } finally { - rmSync(f.base, { recursive: true, force: true }); - } - }); - - it("rejects unknown arguments instead of silently running", () => { - const r = run(["--yolo"], os.tmpdir()); - assert.equal(r.status, 2); - }); -});