Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ export function WebSearchExampleCard({ providerId }: Props) {
const [result, setResult] = useState<{ data: unknown; latencyMs: number } | undefined>();
const [error, setError] = useState<string | null>(null);

const buildBody = () => ({ query, max_results: numResults });
// #13245 — The /api/v1/search route selects providers from body.provider,
// not the x-connection-id header. Send both: the body field drives the
// backend; the header is kept for call-log attribution.
const buildBody = () => ({ query, max_results: numResults, provider: providerId });

const curlSnippet = buildCurl({
endpoint:
Expand Down
23 changes: 23 additions & 0 deletions src/lib/db/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import path from "path";
import { retryProbeIfTransient } from "./probeUtils";
import fs from "fs";
import { resolveWritableDataDir, getLegacyDotDataDir } from "../dataPaths";
import {
MAX_DB_BACKUPS,
DEFAULT_DB_BACKUP_RETENTION_DAYS,
parsePositiveInt,
parseNonNegativeInt,
pruneBackupDirectory,
} from "./backupRetention";
import { isNextBuildPhase } from "../buildPhase";
import { runMigrations } from "./migrationRunner";
import { runDbHealthCheck } from "./healthCheck";
Expand Down Expand Up @@ -883,6 +890,22 @@ function createManagedDbBackup(db: SqliteDatabase, reason: string): boolean {

db.exec(`VACUUM INTO '${escapedBackupPath}'`);
console.log(`[DB] Backup created (${reason}): ${backupPath}`);

// Prune old backups to prevent the directory from growing without bound.
// This mirrors the post-backup pruning in backup.ts but avoids a circular
// dependency by importing directly from backupRetention.ts.
try {
const maxFiles = process.env.DB_BACKUP_MAX_FILES
? parsePositiveInt(process.env.DB_BACKUP_MAX_FILES, MAX_DB_BACKUPS)
: MAX_DB_BACKUPS;
const retentionDays = process.env.DB_BACKUP_RETENTION_DAYS
? parseNonNegativeInt(process.env.DB_BACKUP_RETENTION_DAYS, DEFAULT_DB_BACKUP_RETENTION_DAYS)
: DEFAULT_DB_BACKUP_RETENTION_DAYS;
pruneBackupDirectory({ backupDir, maxFiles, retentionDays });
} catch {
// Retention is best-effort; never let a pruning failure obscure the backup result.
}

return true;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
Expand Down
73 changes: 73 additions & 0 deletions tests/unit/db-backup-healthcheck-prune-13308.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// #13308 — health-check-repair backups were never pruned because the retention
// call was missing from the VACUUM INTO path in core.ts. This test seeds a
// backup directory with more families than MAX_DB_BACKUPS, then runs the same
// pruneBackupDirectory call that createManagedDbBackup now executes after each
// health-check snapshot, and asserts that overflow families are deleted.

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 {
pruneBackupDirectory,
MAX_DB_BACKUPS,
} from "../../src/lib/db/backupRetention.ts";

const serial = { concurrency: false };

function makeBackupDir() {
return fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-backup-prune-"));
}

function seedFamilies(dir: string, count: number) {
for (let i = 0; i < count; i++) {
const ts = new Date(Date.now() - i * 1000).toISOString().replace(/[:.]/g, "-");
const name = `db_${ts}_health-check-repair.sqlite`;
fs.writeFileSync(path.join(dir, name), Buffer.from(`fake-snapshot-${i}`));
}
}

test("#13308 — pruneBackupDirectory removes overflow from health-check-repair path", serial, () => {
const dir = makeBackupDir();
try {
const extra = 5;
seedFamilies(dir, MAX_DB_BACKUPS + extra);

const before = fs.readdirSync(dir).filter((f) => f.endsWith(".sqlite")).length;
assert.ok(before >= MAX_DB_BACKUPS + extra, `seeded ${before} families`);

const result = pruneBackupDirectory({
backupDir: dir,
maxFiles: MAX_DB_BACKUPS,
retentionDays: 0,
});

const after = fs.readdirSync(dir).filter((f) => f.endsWith(".sqlite")).length;
assert.equal(after, MAX_DB_BACKUPS, `pruned to MAX_DB_BACKUPS (${MAX_DB_BACKUPS}), got ${after}`);
assert.equal(result.deletedBackupFamilies, extra, `deleted ${extra} overflow families`);
assert.equal(result.keptBackupFamilies, MAX_DB_BACKUPS);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});

test("#13308 — pruneBackupDirectory is a no-op when under limit", serial, () => {
const dir = makeBackupDir();
try {
seedFamilies(dir, 3);

const result = pruneBackupDirectory({
backupDir: dir,
maxFiles: MAX_DB_BACKUPS,
retentionDays: 0,
});

const after = fs.readdirSync(dir).filter((f) => f.endsWith(".sqlite")).length;
assert.equal(after, 3, "no files removed when under limit");
assert.equal(result.deletedBackupFamilies, 0);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
4 changes: 2 additions & 2 deletions tests/unit/models-catalog-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,9 +1092,9 @@ test("v1 models catalog does not duplicate custom Jina specialty models", async

assert.equal(response.status, 200);
assert.equal(visibleJinaEmbeddingRows.length, 1);
assert.equal(visibleJinaEmbeddingRows[0].id, "jina-ai/jina-embeddings-v5-text-small");
assert.equal(visibleJinaEmbeddingRows[0].id, "jina/jina-embeddings-v5-text-small");
assert.equal(visibleJinaRerankRows.length, 1);
assert.equal(visibleJinaRerankRows[0].id, "jina-ai/jina-reranker-v3");
assert.equal(visibleJinaRerankRows[0].id, "jina/jina-reranker-v3");
});

test("v1 models catalog exposes image model input and output modalities for advanced image providers", async () => {
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/search-provider-body-13245.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// #13245 — WebSearchExampleCard now sends provider in body so
// POST /api/v1/search routes to the correct provider instead of
// silently falling back to the default.

import test from "node:test";
import assert from "node:assert/strict";

// Simulate what buildBody() produces after the fix
function buildBody(query: string, maxResults: number, providerId: string) {
return { query, max_results: maxResults, provider: providerId };
}

const serial = { concurrency: false };

test("#13245 — buildBody includes provider field", serial, () => {
const body = buildBody("test query", 5, "brave-search");
assert.equal(body.provider, "brave-search");
assert.equal(body.query, "test query");
assert.equal(body.max_results, 5);
});

test("#13245 — buildBody provider is the providerId prop", serial, () => {
const body = buildBody("hello", 10, "x-search");
assert.equal(body.provider, "x-search");
});

test("#13245 — provider field prevents silent fallback to default", serial, () => {
const body = buildBody("test", 5, "tavily");
// The route handler checks body.provider first — if present, it resolves
// that provider explicitly instead of auto-selecting the default.
assert.ok(body.provider, "provider must be present to avoid default fallback");
});
Loading