diff --git a/.env.example b/.env.example index 7d92b7cad5..a2156902a6 100644 --- a/.env.example +++ b/.env.example @@ -65,6 +65,12 @@ INITIAL_PASSWORD=CHANGEME # OMNIROUTE_RELEASE_REF=origin/main # OMNIROUTE_ALLOW_CANARY_BUILD=1 +# Build-phase signal (#10060). Set to 1 by scripts/build/build-next-isolated.mjs and +# inherited by every spawned build worker so the DB layer returns a no-op stub instead +# of loading the native better-sqlite3 addon (which aborts the worker on exit). +# Never set this for the running server. Used by: src/lib/buildPhase.ts, src/lib/db/core.ts +# OMNIROUTE_BUILDING=1 + # Encryption key for SQLite database encryption at rest. # Used by: src/lib/db/encryption.ts — encrypts the entire SQLite database. # Generate: openssl rand -hex 32 | Leave empty to disable DB encryption. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 2681ba5478..e9ef0df9c2 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -88,6 +88,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari | `OMNIROUTE_RELEASE_REF` | `origin/main` | `scripts/build/buildProvenance.ts` | Ref the pack-artifact provenance gate checks the build SHA against (#10427). | | `OMNIROUTE_ALLOW_CANARY_BUILD` | _(unset)_ | `scripts/build/buildProvenance.ts` | Set to `1` to allow packing a build whose SHA is not on the release line, recording it as a deliberate canary instead of failing the gate (#10427). | | `OMNIROUTE_SMOKE_API_KEY` | _(unset)_ | `scripts/ops/deploy-canary.mjs` | API key for the canary-deploy smoke probe, sent as `Authorization: Bearer` on `/v1/chat/completions`. Only used by the deploy script (#10429), never by the server. Not related to the `OMNIROUTE_SMOKE_*` variables of the opt-in CLI smoke harness (`RUN_CLI_SMOKE=1`, `OMNIROUTE_SMOKE_BASE_URL/MODEL/API_KEY_ENV/TARGETS/TIMEOUT_MS` in `tests/integration/upstream-cli-smoke.int.test.ts`) — see [CLI Integrations → Real smoke sweep](../guides/CLI-INTEGRATIONS.md). | +| `OMNIROUTE_BUILDING` | _(unset)_ | `src/lib/buildPhase.ts` | Build-phase signal (#10060): set to `1` by `scripts/build/build-next-isolated.mjs` and inherited by every spawned build worker so the DB layer returns a no-op stub instead of loading the native better-sqlite3 addon (which aborts the worker on exit). Never set for the running server. | | `OMNIROUTE_DATA_DIR` | _(unset)_ | `open-sse/executors/promptql/threadSticky.ts` | **Fallback alias** for `DATA_DIR`, checked only when `DATA_DIR` is unset. Used to locate the PromptQL executor's on-disk thread-sticky session cache (`/promptql-thread-sessions.json`); if neither var is set, the cache stays in-memory only (not persisted across restarts). | | `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. | | `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. | diff --git a/stryker.conf.json b/stryker.conf.json index daec377790..f039eeaca8 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -89,7 +89,6 @@ "tests/unit/auth-terminal-status.test.ts", "tests/unit/authz/discovery-routes-local-only.test.ts", "tests/unit/authz/oauth-autoimport-local-only.test.ts", - "tests/unit/quota-exhaustion-cutoff-opencode.test.ts", "tests/unit/authz/route-guard-local-prefix.test.ts", "tests/unit/authz/route-guard-skills-collect.test.ts", "tests/unit/authz/route-guard-version-get-exemption.test.ts", diff --git a/tests/unit/8134-github-t5-fallback-filter.test.ts b/tests/unit/8134-github-t5-fallback-filter.test.ts index 6de8f13416..7f54ce4e34 100644 --- a/tests/unit/8134-github-t5-fallback-filter.test.ts +++ b/tests/unit/8134-github-t5-fallback-filter.test.ts @@ -6,8 +6,8 @@ import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts"; const { getNextFamilyFallback } = await import("../../open-sse/services/modelFamilyFallback.ts"); // Regression for #8134 — GitHub Copilot ("github", alias "gh") T5 family fallback -// returned "claude-opus-4-6" verbatim even though the github registry catalog -// (Opus 4.8 / 4.8-fast / 4.7 / 4.5) has NO 4.6 tier under any dot/hyphen +// returned "claude-opus-4-6" verbatim even though the github registry catalog at +// the time (Opus 4.8 / 4.8-fast / 4.7 / 4.5) had NO 4.6 tier under any dot/hyphen // notation. getNextFamilyFallback() resolved `supportedIds` from the provider's // registry but only used it to try notation variants of a candidate, never to // filter out a candidate that is provably absent from the catalog — so the @@ -18,35 +18,46 @@ const { getNextFamilyFallback } = await import("../../open-sse/services/modelFam // skips (continue) any family candidate that has no match in supportedIds // under ANY notation (hyphen, dot, or a dated-snapshot id with the date // suffix stripped) instead of returning it unfiltered. +// +// Fixture note: #10952 later added claude-opus-4.6 to the github registry, so +// the provably-absent tier used by the fixture moved to claude-opus-4-6-thinking +// (the ladder's first candidate after 4.6 — still absent from the catalog). -test("#8134: github claude-opus-4.8 fallback chain never returns an unsupported tier (claude-opus-4-6)", () => { +test("#8134: github claude-opus fallback chain never returns an unsupported tier (claude-opus-4-6-thinking)", () => { const github = getRegistryEntry("github"); assert.ok(github, "expected the github registry entry to resolve"); const githubIds = new Set(github.models.map((m) => m.id)); + // Fixture assumption: #10952 added claude-opus-4.6 to the github registry, so + // the original absent-tier role moved to the 4.6-thinking variant, which the + // catalog still does NOT carry under any notation. assert.ok( - !githubIds.has("claude-opus-4-6") && !githubIds.has("claude-opus-4.6"), - "fixture assumption broken: github registry now has a 4.6 tier" + !githubIds.has("claude-opus-4-6-thinking") && !githubIds.has("claude-opus-4.6-thinking"), + "fixture assumption broken: github registry now has a 4.6-thinking tier" ); + // Ladder reality: 4.8 -> 4.7 -> 4.6 -> [4-6-thinking (absent), 4-5-20251101, + // sonnet-5]. The absent 4-6-thinking must be SKIPPED — the third hop resolves + // to the dated 4.5 snapshot's undated catalog entry, never to 4-6-thinking. const tried = new Set(["github/claude-opus-4.8"]); - const first = getNextFamilyFallback("github/claude-opus-4.8", tried); - assert.ok(first, "expected a first fallback candidate"); - const firstBareId = first.replace(/^github\//, ""); - assert.ok( - githubIds.has(firstBareId), - `first fallback "${first}" is not in github's registered model catalog: ${[...githubIds].join(", ")}` - ); - - tried.add(first); - const second = getNextFamilyFallback(first, tried); - assert.ok(second, "expected a second fallback candidate (family must not be silently exhausted)"); - const secondBareId = second.replace(/^github\//, ""); - assert.ok( - githubIds.has(secondBareId), - `second fallback "${second}" is not in github's registered model catalog: ${[...githubIds].join(", ")}` - ); - assert.notEqual(secondBareId, "claude-opus-4-6"); - assert.notEqual(secondBareId, "claude-opus-4.6"); + const hops: string[] = []; + let current = "github/claude-opus-4.8"; + for (let hop = 0; hop < 3; hop++) { + const next = getNextFamilyFallback(current, tried); + assert.ok(next, `hop ${hop + 1}: family must not be silently exhausted`); + const bareId = next!.replace(/^github\//, ""); + assert.ok( + githubIds.has(bareId), + `hop ${hop + 1}: "${next}" is not in github's registered model catalog: ${[...githubIds].join(", ")}` + ); + assert.notEqual(bareId, "claude-opus-4-6-thinking"); + assert.notEqual(bareId, "claude-opus-4.6-thinking"); + tried.add(next!); + hops.push(next!); + current = next!; + } + // The skip specifically fired: the 4.6 -> next hop jumped past the absent + // 4-6-thinking tier straight to a catalogued model. + assert.equal(hops[2].replace(/^github\//, ""), "claude-opus-4.5"); }); test("#8134: getNextFamilyFallback never returns a candidate absent from the resolved provider's catalog", () => { diff --git a/tests/unit/cli-tools.test.ts b/tests/unit/cli-tools.test.ts index 123c98e600..4523da96de 100644 --- a/tests/unit/cli-tools.test.ts +++ b/tests/unit/cli-tools.test.ts @@ -106,7 +106,9 @@ test("CLI fingerprint preserves Codex executor User-Agent and maps legacy Copilo { model: "gpt-4o", messages: [] } ); - assert.equal(copilot.headers["User-Agent"], "GitHubCopilotChat/0.54.0"); + // #10952 bumped GITHUB_COPILOT_CLI_VERSION 0.54.0 -> 1.0.81-6; the fingerprint + // pin tracks the advertised upstream CLI version. + assert.equal(copilot.headers["User-Agent"], "GitHubCopilotChat/1.0.81-6"); }); test("CLI fingerprint keeps legacy Copilot settings functional without exposing duplicate UI toggles", () => { diff --git a/tests/unit/db-core-init.test.ts b/tests/unit/db-core-init.test.ts index bb0cd512e5..15ab1b576b 100644 --- a/tests/unit/db-core-init.test.ts +++ b/tests/unit/db-core-init.test.ts @@ -497,7 +497,13 @@ test( } ); -test("build phase uses an in-memory database without creating sqlite files", serial, async () => { +test("build phase returns the no-op stub without creating sqlite files", serial, async () => { + // Contract changed by #10060 (via #10952): the build phase no longer opens a + // real in-memory SQLite with migrations — loading the native better-sqlite3 + // addon aborts the Next.js build worker on exit (node:: + // RemoveEnvironmentCleanupHook). getDbInstance() now returns a no-op stub + // (pinned by tests/unit/build/10060-build-sqlite-stub.test.ts); queries are + // harmless no-ops and no file is touched. const dataDir = makeTempDir("omniroute-db-build-"); try { @@ -510,13 +516,15 @@ test("build phase uses an in-memory database without creating sqlite files", ser const core = await importFresh("src/lib/db/core.ts"); const db = core.getDbInstance(); - assert.ok( + assert.notEqual(db.driver, "better-sqlite3"); + assert.equal( db .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") - .get("provider_connections") + .get("provider_connections"), + undefined, + "the build stub must answer queries with no-ops, never a real table scan" ); assert.equal(fs.existsSync(path.join(dataDir, "storage.sqlite")), false); - assert.equal(db.pragma("journal_mode", { simple: true }), "memory"); core.resetDbInstance(); }