mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 19:02:17 +03:00
* test(infra): retry recursive temp-dir removal on main (main twin of #11968)
`main` has been red since b342c1a361 on the vitest and integration gates:
✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos
Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).
release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.
This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.
The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.
* chore(scripts): carry the rm-maxretries codemod onto main alongside its output
The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.
259 lines
9.5 KiB
TypeScript
259 lines
9.5 KiB
TypeScript
/**
|
|
* Performance regression tests for OmniRoute
|
|
*
|
|
* Tests bulk data operations against acceptable time thresholds.
|
|
* Thresholds are 2x the expected target to account for slow CI machines.
|
|
*
|
|
* Run: node --import tsx --test tests/integration/performance-regression.test.ts
|
|
*/
|
|
|
|
import { describe, it, before, after } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
// --- Environment setup (must come before dynamic imports) ---
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-perf-regression-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
process.env.REQUIRE_API_KEY = "false";
|
|
if (!process.env.API_KEY_SECRET) {
|
|
process.env.API_KEY_SECRET = "test-perf-secret-" + Date.now();
|
|
}
|
|
|
|
// --- Dynamic imports after env setup ---
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const { createApiKey } = await import("../../src/lib/db/apiKeys.ts");
|
|
const { createMemory, listMemories, deleteMemory } = await import("../../src/lib/memory/store.ts");
|
|
const { retrieveMemories } = await import("../../src/lib/memory/retrieval.ts");
|
|
const { MemoryType } = await import("../../src/lib/memory/types.ts");
|
|
const { skillRegistry } = await import("../../src/lib/skills/registry.ts");
|
|
const { GET: memoryRouteGET } = await import("../../src/app/api/memory/route.ts");
|
|
|
|
// --- Work around FTS5 trigger bug ---
|
|
// Migration 022_add_memory_fts5.sql creates FTS5 with content_rowid='id' expecting
|
|
// INTEGER rowids, but memories.id is TEXT (UUID). The triggers fail with
|
|
// SQLITE_MISMATCH on INSERT. Drop them so createMemory() works.
|
|
// Also drop the memory_fts table since FTS5 can't work with TEXT rowids.
|
|
// retrieveMemories() has a fallback that uses keyword scoring when FTS5 is unavailable.
|
|
const _db = core.getDbInstance();
|
|
_db.exec("DROP TRIGGER IF EXISTS memory_fts_ai");
|
|
_db.exec("DROP TRIGGER IF EXISTS memory_fts_ad");
|
|
_db.exec("DROP TRIGGER IF EXISTS memory_fts_au");
|
|
_db.exec("DROP TABLE IF EXISTS memory_fts");
|
|
|
|
// --- Constants ---
|
|
const TEST_API_KEY_ID = "perf-test-api-key";
|
|
const TEST_MACHINE_ID = "perf-test-machine";
|
|
const TEST_SESSION_ID = "perf-test-session";
|
|
const MEMORY_COUNT = 1000;
|
|
const SKILL_COUNT = 100;
|
|
let managementApiKey = "";
|
|
|
|
// --- Thresholds (2x buffer for CI) ---
|
|
const THRESHOLD_LIST_MEMORIES_MS = 200;
|
|
const THRESHOLD_SKILLS_CACHED_MS = 100;
|
|
const THRESHOLD_SKILLS_UNCACHED_MS = 400;
|
|
const THRESHOLD_SEARCH_MS = 400;
|
|
const THRESHOLD_API_ROUTE_MS = 1000;
|
|
|
|
// --- Helpers ---
|
|
function makeMemoryData(index) {
|
|
return {
|
|
apiKeyId: TEST_API_KEY_ID,
|
|
sessionId: TEST_SESSION_ID,
|
|
type: MemoryType.FACTUAL,
|
|
key: `perf-test-key-${index}`,
|
|
content: `This is test memory content number ${index} for performance regression testing purposes`,
|
|
metadata: { index, tag: "perf-test" },
|
|
expiresAt: null,
|
|
};
|
|
}
|
|
|
|
// ============================================================
|
|
// Test 1: listMemories with 1000 records, paginated (page=1, limit=50)
|
|
// ============================================================
|
|
describe("Performance: listMemories pagination (1000 records)", () => {
|
|
const createdIds = [];
|
|
|
|
before(async () => {
|
|
// Bulk insert 1000 memories
|
|
for (let i = 0; i < MEMORY_COUNT; i++) {
|
|
const mem = await createMemory(makeMemoryData(i));
|
|
createdIds.push(mem.id);
|
|
}
|
|
assert.equal(createdIds.length, MEMORY_COUNT, "Should have created 1000 memories");
|
|
});
|
|
|
|
after(async () => {
|
|
// Bulk delete all created memories
|
|
const db = core.getDbInstance();
|
|
db.prepare("DELETE FROM memories WHERE api_key_id = ?").run(TEST_API_KEY_ID);
|
|
});
|
|
|
|
it(`should list page=1, limit=50 of 1000 memories in <${THRESHOLD_LIST_MEMORIES_MS}ms`, async () => {
|
|
const start = performance.now();
|
|
const result = await listMemories({
|
|
apiKeyId: TEST_API_KEY_ID,
|
|
page: 1,
|
|
limit: 50,
|
|
});
|
|
const elapsed = performance.now() - start;
|
|
|
|
assert.equal(result.data.length, 50, "Should return 50 items for page 1");
|
|
assert.equal(result.total, MEMORY_COUNT, "Total should be 1000");
|
|
assert.ok(
|
|
elapsed < THRESHOLD_LIST_MEMORIES_MS,
|
|
`listMemories took ${elapsed.toFixed(1)}ms, expected <${THRESHOLD_LIST_MEMORIES_MS}ms`
|
|
);
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// Test 2: Skills registry - cached vs uncached list
|
|
// ============================================================
|
|
describe("Performance: skills registry cached vs uncached", () => {
|
|
before(async () => {
|
|
// Register 100 skills in the database
|
|
for (let i = 0; i < SKILL_COUNT; i++) {
|
|
await skillRegistry.register({
|
|
name: `perf-skill-${i}`,
|
|
version: "1.0.0",
|
|
description: `Performance test skill ${i}`,
|
|
schema: { input: {}, output: {} },
|
|
handler: `echo "skill ${i}"`,
|
|
enabled: true,
|
|
apiKeyId: TEST_API_KEY_ID,
|
|
});
|
|
}
|
|
});
|
|
|
|
after(async () => {
|
|
// Clean up skills
|
|
const db = core.getDbInstance();
|
|
db.prepare("DELETE FROM skills WHERE api_key_id = ?").run(TEST_API_KEY_ID);
|
|
skillRegistry.invalidateCache();
|
|
});
|
|
|
|
it(`should load skills from DB (uncached) in <${THRESHOLD_SKILLS_UNCACHED_MS}ms`, async () => {
|
|
// Force cache invalidation so loadFromDatabase actually hits DB
|
|
skillRegistry.invalidateCache();
|
|
|
|
const start = performance.now();
|
|
await skillRegistry.loadFromDatabase();
|
|
const elapsed = performance.now() - start;
|
|
|
|
assert.ok(
|
|
elapsed < THRESHOLD_SKILLS_UNCACHED_MS,
|
|
`Uncached loadFromDatabase took ${elapsed.toFixed(1)}ms, expected <${THRESHOLD_SKILLS_UNCACHED_MS}ms`
|
|
);
|
|
});
|
|
|
|
it(`should list skills from cache in <${THRESHOLD_SKILLS_CACHED_MS}ms`, async () => {
|
|
// Ensure cache is warm (loadFromDatabase was just called above)
|
|
// Call list() which reads from in-memory Map
|
|
const start = performance.now();
|
|
const skills = skillRegistry.list();
|
|
const elapsed = performance.now() - start;
|
|
|
|
assert.ok(skills.length >= SKILL_COUNT, `Should have at least ${SKILL_COUNT} skills`);
|
|
assert.ok(
|
|
elapsed < THRESHOLD_SKILLS_CACHED_MS,
|
|
`Cached list() took ${elapsed.toFixed(1)}ms, expected <${THRESHOLD_SKILLS_CACHED_MS}ms`
|
|
);
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// Test 3: Search over 1000 memories (keyword scoring fallback)
|
|
//
|
|
// Note: FTS5 triggers are dropped due to a content_rowid bug (TEXT vs INTEGER).
|
|
// retrieveMemories() falls back to chronological + getRelevanceScore() keyword
|
|
// scoring, which is the production fallback path we validate here.
|
|
// ============================================================
|
|
describe("Performance: memory search (1000 records)", () => {
|
|
const createdIds = [];
|
|
|
|
before(async () => {
|
|
// Bulk insert 1000 memories with searchable content
|
|
for (let i = 0; i < MEMORY_COUNT; i++) {
|
|
const mem = await createMemory(makeMemoryData(i));
|
|
createdIds.push(mem.id);
|
|
}
|
|
assert.equal(createdIds.length, MEMORY_COUNT, "Should have created 1000 memories");
|
|
});
|
|
|
|
after(async () => {
|
|
const db = core.getDbInstance();
|
|
db.prepare("DELETE FROM memories WHERE api_key_id = ?").run(TEST_API_KEY_ID);
|
|
});
|
|
|
|
it(`should search memories with retrieveMemories (query="test") in <${THRESHOLD_SEARCH_MS}ms`, async () => {
|
|
const start = performance.now();
|
|
const results = await retrieveMemories(TEST_API_KEY_ID, {
|
|
query: "test",
|
|
retrievalStrategy: "semantic",
|
|
maxTokens: 8000,
|
|
});
|
|
const elapsed = performance.now() - start;
|
|
|
|
assert.ok(results.length > 0, "Should find matching memories");
|
|
assert.ok(
|
|
elapsed < THRESHOLD_SEARCH_MS,
|
|
`retrieveMemories search took ${elapsed.toFixed(1)}ms, expected <${THRESHOLD_SEARCH_MS}ms`
|
|
);
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// Test 4: API route handler GET /api/memory?limit=50
|
|
// ============================================================
|
|
describe("Performance: memory API route handler (1000 records)", () => {
|
|
before(async () => {
|
|
const key = await createApiKey("performance regression management", TEST_MACHINE_ID, [
|
|
"manage",
|
|
]);
|
|
managementApiKey = key.key;
|
|
|
|
// Bulk insert 1000 memories
|
|
for (let i = 0; i < MEMORY_COUNT; i++) {
|
|
await createMemory(makeMemoryData(i));
|
|
}
|
|
});
|
|
|
|
after(async () => {
|
|
const db = core.getDbInstance();
|
|
db.prepare("DELETE FROM memories WHERE api_key_id = ?").run(TEST_API_KEY_ID);
|
|
// Final cleanup: reset DB instance and remove temp dir
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
});
|
|
|
|
it(`should handle GET /api/memory?limit=50 in <${THRESHOLD_API_ROUTE_MS}ms`, async () => {
|
|
// Create a mock Request object for the route handler
|
|
const request = new Request(
|
|
`http://localhost:20128/api/memory?limit=50&apiKeyId=${TEST_API_KEY_ID}`,
|
|
{
|
|
method: "GET",
|
|
headers: { authorization: `Bearer ${managementApiKey}` },
|
|
}
|
|
);
|
|
|
|
const start = performance.now();
|
|
const response = await memoryRouteGET(request);
|
|
const elapsed = performance.now() - start;
|
|
|
|
assert.equal(response.status, 200, "Response should be 200 OK");
|
|
|
|
const body = (await response.json()) as any;
|
|
assert.equal(body.data.length, 50, "Should return 50 items");
|
|
assert.equal(body.total, MEMORY_COUNT, "Total should be 1000");
|
|
assert.ok(body.stats, "Response should include stats");
|
|
|
|
assert.ok(
|
|
elapsed < THRESHOLD_API_ROUTE_MS,
|
|
`API route handler took ${elapsed.toFixed(1)}ms, expected <${THRESHOLD_API_ROUTE_MS}ms`
|
|
);
|
|
});
|
|
});
|