mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 15:42:12 +03:00
⭐5 — Fecha 11 achados restantes das advisories de segurança do v3.8.50 (batch 2), TDD. UNSTABLE é o base-red #9985 já rastreado.
224 lines
9.3 KiB
TypeScript
224 lines
9.3 KiB
TypeScript
/**
|
|
* tests/unit/mcp-memory-tools-strategy.test.ts
|
|
*
|
|
* Plan 21 F8 — D16: omniroute_memory_search reads retrievalStrategy from settings.
|
|
*
|
|
* Since Node 20 does not support mock.module() for ESM, we test:
|
|
* A) toMemoryRetrievalConfig mapping: strategy="hybrid" → retrievalStrategy="hybrid"
|
|
* B) toMemoryRetrievalConfig mapping: strategy="semantic" → retrievalStrategy="semantic"
|
|
* C) toMemoryRetrievalConfig mapping: strategy="recent" → retrievalStrategy="exact"
|
|
* D) handler end-to-end with strategy="hybrid" in DB → handler returns success
|
|
* E) handler end-to-end with strategy="recent" in DB → handler returns success (fallback to "exact")
|
|
* F) getMemorySettings() failure fallback: toMemoryRetrievalConfig is not called;
|
|
* handler uses hardcoded fallback config with retrievalStrategy="exact"
|
|
*/
|
|
|
|
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";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-mcp-strategy-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
|
process.env.VECTOR_STORE_DISABLE_VEC = "true";
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
|
|
function cleanup() {
|
|
core.resetDbInstance();
|
|
if (fs.existsSync(TEST_DATA_DIR)) {
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
}
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
test.afterEach(() => cleanup());
|
|
test.after(() => {
|
|
if (fs.existsSync(TEST_DATA_DIR)) {
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// ── A: toMemoryRetrievalConfig: "hybrid" → retrievalStrategy="hybrid" ─────────
|
|
|
|
test("toMemoryRetrievalConfig: strategy=hybrid → retrievalStrategy=hybrid", async () => {
|
|
const { toMemoryRetrievalConfig, DEFAULT_MEMORY_SETTINGS } = await import(
|
|
"../../src/lib/memory/settings.ts"
|
|
);
|
|
const settings = { ...DEFAULT_MEMORY_SETTINGS, strategy: "hybrid" as const };
|
|
const config = toMemoryRetrievalConfig(settings);
|
|
assert.equal(
|
|
config.retrievalStrategy,
|
|
"hybrid",
|
|
"hybrid strategy must map to retrievalStrategy=hybrid"
|
|
);
|
|
});
|
|
|
|
// ── B: toMemoryRetrievalConfig: "semantic" → retrievalStrategy="semantic" ─────
|
|
|
|
test("toMemoryRetrievalConfig: strategy=semantic → retrievalStrategy=semantic", async () => {
|
|
const { toMemoryRetrievalConfig, DEFAULT_MEMORY_SETTINGS } = await import(
|
|
"../../src/lib/memory/settings.ts"
|
|
);
|
|
const settings = { ...DEFAULT_MEMORY_SETTINGS, strategy: "semantic" as const };
|
|
const config = toMemoryRetrievalConfig(settings);
|
|
assert.equal(
|
|
config.retrievalStrategy,
|
|
"semantic",
|
|
"semantic strategy must map to retrievalStrategy=semantic"
|
|
);
|
|
});
|
|
|
|
// ── C: toMemoryRetrievalConfig: "recent" → retrievalStrategy="exact" ──────────
|
|
|
|
test("toMemoryRetrievalConfig: strategy=recent → retrievalStrategy=exact (mapped)", async () => {
|
|
const { toMemoryRetrievalConfig, DEFAULT_MEMORY_SETTINGS } = await import(
|
|
"../../src/lib/memory/settings.ts"
|
|
);
|
|
const settings = { ...DEFAULT_MEMORY_SETTINGS, strategy: "recent" as const };
|
|
const config = toMemoryRetrievalConfig(settings);
|
|
assert.equal(
|
|
config.retrievalStrategy,
|
|
"exact",
|
|
"recent strategy must map to retrievalStrategy=exact"
|
|
);
|
|
});
|
|
|
|
// ── D: handler end-to-end with strategy="hybrid" in DB ────────────────────────
|
|
|
|
test("omniroute_memory_search: strategy=hybrid in DB → handler returns success", async () => {
|
|
const db = core.getDbInstance();
|
|
|
|
// Seed a memory to ensure retrieval has something to work with
|
|
db.prepare(
|
|
`INSERT INTO memories (id, api_key_id, session_id, type, key, content, metadata, created_at, updated_at, expires_at)
|
|
VALUES ('mcp-h1', 'api-mcp-h', '', 'factual', 'key-h1', 'Paris is the capital of France', '{}', datetime('now'), datetime('now'), NULL)`
|
|
).run();
|
|
|
|
// Set memoryStrategy = "hybrid" in settings
|
|
db.prepare(
|
|
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', 'memoryStrategy', ?)"
|
|
).run(JSON.stringify("hybrid"));
|
|
|
|
const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts");
|
|
invalidateMemorySettingsCache();
|
|
|
|
const { memoryTools } = await import(
|
|
"../../open-sse/mcp-server/tools/memoryTools.ts"
|
|
);
|
|
const handler = memoryTools.omniroute_memory_search.handler;
|
|
|
|
const result = await handler({ apiKeyId: "api-mcp-h", query: "Paris" });
|
|
|
|
assert.equal(result.success, true, "handler must return success=true");
|
|
assert.ok(typeof result.data.count === "number", "data.count must be a number");
|
|
assert.ok(Array.isArray(result.data.memories), "data.memories must be an array");
|
|
});
|
|
|
|
// ── E: handler end-to-end with strategy="recent" in DB ────────────────────────
|
|
|
|
test("omniroute_memory_search: strategy=recent in DB → handler maps to exact, returns success", async () => {
|
|
const db = core.getDbInstance();
|
|
|
|
db.prepare(
|
|
`INSERT INTO memories (id, api_key_id, session_id, type, key, content, metadata, created_at, updated_at, expires_at)
|
|
VALUES ('mcp-r1', 'api-mcp-r', '', 'factual', 'key-r1', 'Berlin is the capital of Germany', '{}', datetime('now'), datetime('now'), NULL)`
|
|
).run();
|
|
|
|
// Set memoryStrategy = "recent"
|
|
db.prepare(
|
|
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', 'memoryStrategy', ?)"
|
|
).run(JSON.stringify("recent"));
|
|
|
|
const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts");
|
|
invalidateMemorySettingsCache();
|
|
|
|
const { memoryTools } = await import(
|
|
"../../open-sse/mcp-server/tools/memoryTools.ts"
|
|
);
|
|
const handler = memoryTools.omniroute_memory_search.handler;
|
|
|
|
const result = await handler({ apiKeyId: "api-mcp-r" });
|
|
|
|
assert.equal(result.success, true, "handler must return success=true even with strategy=recent");
|
|
assert.ok(Array.isArray(result.data.memories), "data.memories must be an array");
|
|
});
|
|
|
|
// ── F: fallback path — DEFAULT_MEMORY_SETTINGS has strategy "hybrid" (default)
|
|
// toMemoryRetrievalConfig used on DEFAULT maps to retrievalStrategy="hybrid" ──
|
|
|
|
test("toMemoryRetrievalConfig: DEFAULT_MEMORY_SETTINGS maps to retrievalStrategy=hybrid", async () => {
|
|
const { toMemoryRetrievalConfig, DEFAULT_MEMORY_SETTINGS } = await import(
|
|
"../../src/lib/memory/settings.ts"
|
|
);
|
|
// Verify the default strategy is "hybrid" so fallback in handler resolves to hybrid
|
|
assert.equal(
|
|
DEFAULT_MEMORY_SETTINGS.strategy,
|
|
"hybrid",
|
|
"DEFAULT_MEMORY_SETTINGS.strategy must be 'hybrid'"
|
|
);
|
|
const config = toMemoryRetrievalConfig(DEFAULT_MEMORY_SETTINGS);
|
|
assert.equal(
|
|
config.retrievalStrategy,
|
|
"hybrid",
|
|
"default settings must map to retrievalStrategy=hybrid"
|
|
);
|
|
});
|
|
|
|
// ── G: handler fallback when getMemorySettings throws — uses hardcoded "exact" ─
|
|
|
|
test("omniroute_memory_search: hardcoded fallback config has retrievalStrategy=exact", async () => {
|
|
// This tests the fallback branch in the handler (catch(() => null) path).
|
|
// We verify this by examining the fallback object directly from the source logic:
|
|
// When memorySettings is null, the handler uses retrievalStrategy: "exact" as const.
|
|
// We test this via toMemoryRetrievalConfig with a minimal disabled-settings object.
|
|
const { toMemoryRetrievalConfig, DEFAULT_MEMORY_SETTINGS } = await import(
|
|
"../../src/lib/memory/settings.ts"
|
|
);
|
|
|
|
// Simulate the catch path: strategy "recent" maps to "exact" (same as hardcoded fallback)
|
|
const disabledSettings = { ...DEFAULT_MEMORY_SETTINGS, strategy: "recent" as const };
|
|
const config = toMemoryRetrievalConfig(disabledSettings);
|
|
assert.equal(
|
|
config.retrievalStrategy,
|
|
"exact",
|
|
"fallback from catch path must use retrievalStrategy=exact"
|
|
);
|
|
});
|
|
|
|
// ── IDOR: the authenticated caller's principal must win over a caller-supplied
|
|
// apiKeyId (GHSA-cpv3-xr7r-xf8q). With a resolvable caller (here: OMNIROUTE_API_KEY
|
|
// on the stdio path → "env-key"), omniroute_memory_add must store under the
|
|
// caller, NOT under the arbitrary apiKeyId in the tool arguments.
|
|
test("omniroute_memory_add: caller principal wins over a spoofed apiKeyId (GHSA-cpv3)", async () => {
|
|
const db = core.getDbInstance();
|
|
const prevEnvKey = process.env.OMNIROUTE_API_KEY;
|
|
process.env.OMNIROUTE_API_KEY = "test-mcp-caller-key";
|
|
try {
|
|
const { memoryTools } = await import("../../open-sse/mcp-server/tools/memoryTools.ts");
|
|
const result = await memoryTools.omniroute_memory_add.handler({
|
|
apiKeyId: "victim-b",
|
|
type: "factual",
|
|
key: "idor-k1",
|
|
content: "owned-by-caller",
|
|
});
|
|
assert.equal(result.success, true, "add must succeed");
|
|
|
|
const rows = db
|
|
.prepare("SELECT api_key_id FROM memories WHERE key = 'idor-k1'")
|
|
.all() as Array<{ api_key_id: string }>;
|
|
assert.equal(rows.length, 1, "exactly one memory row expected");
|
|
assert.equal(
|
|
rows[0].api_key_id,
|
|
"env-key",
|
|
"memory must be stored under the resolved caller (env-key), not the spoofed apiKeyId"
|
|
);
|
|
assert.notEqual(rows[0].api_key_id, "victim-b", "must NOT store under the caller-supplied id");
|
|
} finally {
|
|
if (prevEnvKey === undefined) delete process.env.OMNIROUTE_API_KEY;
|
|
else process.env.OMNIROUTE_API_KEY = prevEnvKey;
|
|
}
|
|
});
|