mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
fix(memory): register the sqlite backend on the /api/memory/[id] route — every handler 500'd (#9737) (#9785)
* fix(api): validate request bodies with Zod in 4 routes — restores the t06 gate The release-green verdict (#9737) lists check:route-validation:t06 as a HARD failure and it is STILL red on the current tip: four routes call request.json() and hand-roll `typeof x === "string"` checks instead of using Zod, which Hard Rule #7 requires and the gate enforces (it scans source and has no allowlist). - src/app/api/plugins/marketplace/install (#9445): InstallBodySchema; the 400 'Missing or invalid name field' response is preserved verbatim. - src/app/api/services/dario/admin/accounts (#8523): DeleteAccountBodySchema for the optional { alias } DELETE body; query-param path untouched. - src/app/api/services/dario/admin/login-start (#8523): LoginStartBodySchema; trimming now happens in the schema, so the forward body is unchanged. - src/app/api/services/dario/admin/import-from-omniroute (#8523): ImportBodySchema for connectionId/alias; invalid shapes fall back to the same 'connectionId is required' 400 as before. All four keep their exact status codes and messages — this is a validation mechanism swap, not a contract change (plugins route suite still 33/33). Adds tests/unit/route-body-validation-t06.test.ts, which runs the gate's own rule inside the unit suite so the next such route fails on ITS OWN PR instead of surfacing weeks later in a base-red sweep. Guard verified by mutation: renaming .safeParse( in one route makes it fail (1 fail), restored from a pre-probe copy. Gates: route-validation:t06, file-size, test-discovery, mutation-test-coverage, dead-code exit 0; typecheck:core clean; eslint clean. Refs #9737 * fix(memory): register the sqlite backend on the /api/memory/[id] route — every handler 500'd GET/PUT/DELETE /api/memory/[id] threw `Primary backend "sqlite" not registered` and returned 500. #8752 (MemoryBackend provider pattern) wired the route to `@/lib/memory/manager` directly, but the registry is populated by an import-time side effect in the module INDEX (src/lib/memory/index.ts:23, `memoryManager.register(sqliteBackend)`). Importing the bare manager gives an empty registry. In production the failure is order-dependent, which is why it went unnoticed: if /api/memory (which imports the index) is hit first in the same process, the singleton is already populated and [id] works. Reached first — the common case for a client that edits a known memory id — every request 500s. The sibling route is the only other consumer and already imports the index; this was the lone direct-manager import in src/. - Fix: import from `@/lib/memory` (index) with a comment stating WHY the indirection matters, so the next refactor does not simplify it back. - Guard: tests/integration/memory-route-put.test.ts already covered this and was failing 2/5 on the base (it only surfaced now because the integration suite runs on the release-PR CI, not per-PR). Now 5/5. Also fixes a test-isolation defect in the same run: tests/integration/combo-matrix/context-relay-codex.test.ts reused one combo name across both tests, and the control failed with `UNIQUE constraint failed: combos.name` — resetStorage() unlinks the DB file but the previous better-sqlite3 handle keeps writing to the same inode. Gave the control its own combo name and parameterized the request builder; the assertion is unchanged (it never depended on the name). 2/2. Integration suite on this tip: 936 tests, 32m19s — under the 40min ceiling the old verdict reported as exceeded (#9737 item 6), which the migration-135 collision was causing. Refs #9737 --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
24bdae29ca
commit
c7e20e95de
1
changelog.d/fixes/9737-memory-id-route-backend.md
Normal file
1
changelog.d/fixes/9737-memory-id-route-backend.md
Normal file
@@ -0,0 +1 @@
|
||||
- Fixed `GET`/`PUT`/`DELETE /api/memory/[id]` always failing with a 500 (`Primary backend "sqlite" not registered`) when the route was reached before any other memory endpoint in the same process.
|
||||
@@ -1,6 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { memoryManager } from "@/lib/memory/manager";
|
||||
// Import through the module index, NOT "@/lib/memory/manager" directly: the index's
|
||||
// import-time side effect is what calls memoryManager.register(sqliteBackend). Importing
|
||||
// the bare manager gives an EMPTY registry, so every handler here threw
|
||||
// `Primary backend "sqlite" not registered` and returned 500 (#8752).
|
||||
import { memoryManager } from "@/lib/memory";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
import { MemoryUpdatePutSchema } from "@/shared/schemas/memory";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
|
||||
|
||||
@@ -62,6 +62,12 @@ const { getHandoff } = await import("../../../src/lib/db/contextHandoffs.ts");
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const CODEX_COMBO_NAME = "m-relay-codex-quota";
|
||||
// The control test needs its OWN combo name. resetStorage() unlinks the DB file
|
||||
// between tests, but the previous better-sqlite3 handle survives the unlink and
|
||||
// keeps writing to the same inode, so reusing the name here failed with
|
||||
// `UNIQUE constraint failed: combos.name` — a test-isolation defect, not a
|
||||
// routing one (the assertion below does not depend on the name).
|
||||
const CONTROL_COMBO_NAME = "m-relay-openai-control";
|
||||
const SESSION_HEADER_VALUE = "relay-codex-quota-001";
|
||||
const SESSION_ID = `ext:${SESSION_HEADER_VALUE}`;
|
||||
|
||||
@@ -71,8 +77,7 @@ const CODEX_RESPONSES_HOST = "chatgpt.com/backend-api/codex/responses";
|
||||
|
||||
// Summary JSON that parseHandoffJSON will successfully parse.
|
||||
const CODEX_SUMMARY_JSON = JSON.stringify({
|
||||
summary:
|
||||
"User is implementing a TypeScript context-relay codex quota-handoff test using TDD.",
|
||||
summary: "User is implementing a TypeScript context-relay codex quota-handoff test using TDD.",
|
||||
keyDecisions: ["codex provider selected", "quota threshold at 90%"],
|
||||
taskProgress: "writing deterministic integration test for codex handoff",
|
||||
activeEntities: ["combo.ts", "codexQuotaFetcher.ts", "contextHandoff.ts"],
|
||||
@@ -142,11 +147,11 @@ function buildCodexUsageBody(
|
||||
|
||||
// ── Request builder ───────────────────────────────────────────────────────────
|
||||
|
||||
function codexRequest(withSessionId = true) {
|
||||
function codexRequest(withSessionId = true, comboName = CODEX_COMBO_NAME) {
|
||||
return buildRequest({
|
||||
headers: withSessionId ? { "x-session-id": SESSION_HEADER_VALUE } : {},
|
||||
body: {
|
||||
model: CODEX_COMBO_NAME,
|
||||
model: comboName,
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: "Write a TypeScript hello world." }],
|
||||
},
|
||||
@@ -259,9 +264,7 @@ test("context-relay codex quota handoff: fires and expiresAt matches session-win
|
||||
name: CODEX_COMBO_NAME,
|
||||
strategy: "context-relay",
|
||||
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
|
||||
models: [
|
||||
{ id: "rc-codex-1", kind: "model", providerId: "codex", model: "gpt-5.3-codex" },
|
||||
],
|
||||
models: [{ id: "rc-codex-1", kind: "model", providerId: "codex", model: "gpt-5.3-codex" }],
|
||||
});
|
||||
|
||||
// 4. Compute quota reset times (future timestamps).
|
||||
@@ -328,12 +331,10 @@ test("context-relay codex quota handoff: does NOT fire when provider is openai (
|
||||
await seedConnection("openai", { apiKey: "sk-openai-control-no-codex-block" });
|
||||
|
||||
await combosDb.createCombo({
|
||||
name: CODEX_COMBO_NAME,
|
||||
name: CONTROL_COMBO_NAME,
|
||||
strategy: "context-relay",
|
||||
config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 },
|
||||
models: [
|
||||
{ id: "rc-openai-ctrl", kind: "model", providerId: "openai", model: "gpt-4o-mini" },
|
||||
],
|
||||
models: [{ id: "rc-openai-ctrl", kind: "model", providerId: "openai", model: "gpt-4o-mini" }],
|
||||
});
|
||||
|
||||
const seenUrls: string[] = [];
|
||||
@@ -342,7 +343,7 @@ test("context-relay codex quota handoff: does NOT fire when provider is openai (
|
||||
return buildOpenAIResponse("assistant reply ok");
|
||||
};
|
||||
|
||||
const r = await handleChat(codexRequest(true));
|
||||
const r = await handleChat(codexRequest(true, CONTROL_COMBO_NAME));
|
||||
assert.equal(r.status, 200, "openai request must return 200");
|
||||
|
||||
// Give setImmediate time to fire if the block were incorrectly entered.
|
||||
@@ -358,7 +359,7 @@ test("context-relay codex quota handoff: does NOT fire when provider is openai (
|
||||
// No codex quota handoff record in DB.
|
||||
// (The universal handoff also does not fire because no prior model is seeded,
|
||||
// so getLastSessionModel returns null → no model switch detected.)
|
||||
const handoff = getHandoff(SESSION_ID, CODEX_COMBO_NAME);
|
||||
const handoff = getHandoff(SESSION_ID, CONTROL_COMBO_NAME);
|
||||
assert.equal(
|
||||
handoff,
|
||||
null,
|
||||
|
||||
Reference in New Issue
Block a user