Files
OmniRoute/tests/unit/cli-memory-commands.test.ts
diegosouzapw 516a7e5520 fix(memory): code-review hardening — guards, asserts, useEffect
Smaller fixes from the 2nd code-review pass on plan 21.

Backend / CLI / DB:
- memoryTools.ts: error-path fallback no longer hardcodes
  retrievalStrategy:"exact"; uses DEFAULT_MEMORY_SETTINGS via
  toMemoryRetrievalConfig (D16 / Bug #7).
- memory.mjs: applyLegacyTypeMap also runs on search / list / clear
  (was only on add); legacy user/feedback/project/reference remap to
  canonical types with a stderr warning (D17 / Bug #4).
- migrationRunner.ts: case "073" guards via
  hasColumn(memories, needs_reindex) so an unmarked re-run of
  073_memory_vec.sql is skipped cleanly (D27).

UI:
- MemoryEngineStatus: optional onConfigure callback; "Configurar →"
  CTAs on the Embedding / Qdrant / Rerank rows when those components
  are off or missing (matches §4.3 wireframe).
- EngineTab: scroll IDs on config cards + handleConfigure wired to
  the status panel. Providers fetch moved from render body
  (setState-during-render anti-pattern) into useEffect.
- RerankConfigCard: toggle is disabled when no provider has a key —
  blocks turning rerank ON without a provider, still allows turning
  it OFF (D13).
- MemoriesTab: Import validates each entry against the canonical
  type enum before POST so invalid types are caught locally with a
  clear skipped count.

Tooling:
- package.json: test:all includes test:vitest:ui so the UI suite
  is no longer orphaned in CI.

Tests:
- cli-memory-commands: asserts updated for the new legacy->canonical
  remap on search/clear.
- memory-embedding-resolve: drop always-true `|| reason.length > 0`
  clauses that neutralized two assertions.
- memory-embedding-static-potion: model_load_failed test forces a
  real load failure via MEMORY_STATIC_CACHE_DIR=/dev/null/<subdir>
  and asserts EmbeddingError shape + reason + sanitized message
  (replaces the previous `assert.ok(true)`).
- rerank-config-card.test.tsx: happy-path now uses a provider with
  hasKey; new test covers the disabled-toggle guard.

Full memory suite green: 331/331 unit tests, 46/46 UI tests.
typecheck:core, typecheck:noimplicit:core, check:cycles clean.
2026-05-28 21:46:54 -03:00

188 lines
6.1 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const MEMORY_ITEMS = [
{
id: "mem_001",
type: "user",
content: "User prefers dark mode",
score: 0.95,
createdAt: "2026-05-10T10:00:00Z",
},
{
id: "mem_002",
type: "project",
content: "Project uses Next.js 16",
score: 0.88,
createdAt: "2026-05-09T12:00:00Z",
},
];
function makeResp(data: unknown, status = 200) {
const obj = {
ok: status < 400,
status,
exitCode: status < 400 ? 0 : 1,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)),
headers: new Headers(),
};
obj.json = obj.json.bind(obj);
obj.text = obj.text.bind(obj);
return obj;
}
async function captureStdout(fn: () => Promise<void>): Promise<string> {
const chunks: string[] = [];
const orig = process.stdout.write.bind(process.stdout);
process.stdout.write = (c: string | Uint8Array) => {
if (typeof c === "string") chunks.push(c);
return true;
};
try {
await fn();
} finally {
process.stdout.write = orig;
}
return chunks.join("");
}
function makeCmd(output = "json") {
return { optsWithGlobals: () => ({ output, quiet: output !== "table" }) };
}
test("runMemorySearch retorna items como array JSON", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = (() => Promise.resolve(makeResp({ items: MEMORY_ITEMS }))) as any;
const { runMemorySearch } = await import("../../bin/cli/commands/memory.mjs");
const out = await captureStdout(() =>
runMemorySearch("dark mode", { limit: 20 }, makeCmd() as any)
);
globalThis.fetch = origFetch;
const parsed = JSON.parse(out);
assert.ok(Array.isArray(parsed));
assert.equal(parsed[0].id, "mem_001");
assert.equal(parsed[0].type, "user");
});
test("runMemorySearch envia q e type na query", async () => {
let capturedUrl = "";
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string) => {
capturedUrl = url;
return Promise.resolve(makeResp({ items: MEMORY_ITEMS }));
}) as any;
const { runMemorySearch } = await import("../../bin/cli/commands/memory.mjs");
await captureStdout(() =>
runMemorySearch("react hooks", { limit: 10, type: "project" }, makeCmd() as any)
);
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("q=react") && capturedUrl.includes("hooks"));
// Plan 21 / D17: legacy 'project' is remapped to canonical 'factual' by
// applyLegacyTypeMap in the CLI before reaching the backend.
assert.ok(capturedUrl.includes("type=factual"));
assert.ok(!capturedUrl.includes("type=project"));
assert.ok(capturedUrl.includes("limit=10"));
});
test("runMemoryAdd envia POST com content e type", async () => {
// Plan 21 / D17: legacy 'user' type is mapped to canonical 'factual'.
let capturedUrl = "";
let capturedInit: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, init: any) => {
capturedUrl = url;
capturedInit = init;
return Promise.resolve(
makeResp({ id: "mem_new", type: "factual", content: "test content" })
);
}) as any;
const { runMemoryAdd } = await import("../../bin/cli/commands/memory.mjs");
await captureStdout(() =>
runMemoryAdd({ content: "test content", type: "user" }, makeCmd() as any)
);
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("/api/memory"));
assert.equal(capturedInit?.method, "POST");
const body = JSON.parse(capturedInit?.body);
assert.equal(body.content, "test content");
// Legacy 'user' is remapped to canonical 'factual' by CLI (plan 21 / D17).
assert.equal(body.type, "factual");
});
test("runMemoryList retorna items sem q", async () => {
let capturedUrl = "";
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string) => {
capturedUrl = url;
return Promise.resolve(makeResp({ items: MEMORY_ITEMS }));
}) as any;
const { runMemoryList } = await import("../../bin/cli/commands/memory.mjs");
const out = await captureStdout(() => runMemoryList({ limit: 100 }, makeCmd() as any));
globalThis.fetch = origFetch;
assert.ok(!capturedUrl.includes("q="));
const parsed = JSON.parse(out);
assert.ok(Array.isArray(parsed));
assert.equal(parsed.length, 2);
});
test("runMemoryGet busca /api/memory/:id", async () => {
let capturedUrl = "";
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string) => {
capturedUrl = url;
return Promise.resolve(makeResp(MEMORY_ITEMS[0]));
}) as any;
const { runMemoryGet } = await import("../../bin/cli/commands/memory.mjs");
const out = await captureStdout(() => runMemoryGet("mem_001", {}, makeCmd() as any));
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("/api/memory/mem_001"));
const parsed = JSON.parse(out);
assert.equal(parsed.id, "mem_001");
});
test("runMemoryHealth retorna status do subsistema", async () => {
const origFetch = globalThis.fetch;
globalThis.fetch = (() =>
Promise.resolve(makeResp({ status: "healthy", fts5: true, qdrant: true }))) as any;
const { runMemoryHealth } = await import("../../bin/cli/commands/memory.mjs");
const out = await captureStdout(() => runMemoryHealth({}, makeCmd() as any));
globalThis.fetch = origFetch;
const parsed = JSON.parse(out);
assert.equal(parsed.status, "healthy");
});
test("runMemoryClear --yes envia DELETE com filtro de type", async () => {
let capturedUrl = "";
let capturedInit: any = null;
const origFetch = globalThis.fetch;
globalThis.fetch = ((url: string, init: any) => {
capturedUrl = url;
capturedInit = init;
return Promise.resolve(makeResp({ deleted: 5 }));
}) as any;
const { runMemoryClear } = await import("../../bin/cli/commands/memory.mjs");
await captureStdout(() => runMemoryClear({ yes: true, type: "project" }, makeCmd() as any));
globalThis.fetch = origFetch;
assert.ok(capturedUrl.includes("/api/memory"));
assert.equal(capturedInit?.method, "DELETE");
// Plan 21 / D17: legacy 'project' is remapped to canonical 'factual' by
// applyLegacyTypeMap in the CLI before reaching the backend.
assert.ok(capturedUrl.includes("type=factual"));
assert.ok(!capturedUrl.includes("type=project"));
});