Files
OmniRoute/tests/unit/memory-extraction.test.mjs
oyi77 e6e54822f5 feat: add Memory & Skill Injection from Proxy (Network Level)
Implements transparent memory and skill injection at the proxy layer,
enabling AI agents connecting through OmniRoute to automatically inherit
memory and tool capabilities without client-side code changes.

Memory System:
- SQLite-backed memory store with 4 types: factual, episodic, procedural, semantic
- Token budget retrieval with configurable max tokens (default 2000)
- Memory injection into chat requests (system message + message prefix)
- Memory fact extraction from responses
- Dashboard page: /dashboard/memory
- MCP tools: omniroute_memory_search, omniroute_memory_add, omniroute_memory_clear

Skills System:
- Skill registry with semver resolution (^, ~, >=, etc.)
- Docker sandbox runner with resource constraints (CPU 100ms, RAM 256MB)
- Skill executor with timeout handling
- Built-in skills: file_read, file_write, http_request, web_search, eval_code, execute_command
- Skill schema injection for OpenAI, Claude, Gemini formats
- Tool call interception and execution
- Dashboard page: /dashboard/skills
- MCP tools: omniroute_skills_list, omniroute_skills_enable, omniroute_skills_execute

Advanced Features:
- Browser automation skill (Playwright-based)
- A2A memory-aware routing skill
- Hybrid execution mode (direct/sandbox/auto-upgrade)
- Custom skill registration API
- Integration tests
- Memory caching layer
- Memory summarization
- Performance benchmarks

Resolves: GitHub Issue #812
2026-04-01 09:26:37 +07:00

170 lines
7.3 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
const { extractFactsFromText, extractFacts } = await import("../../src/lib/memory/extraction.ts");
// ─── extractFactsFromText: Preferences ─────────────────────────────────────
test("extractFactsFromText: detects 'I prefer' preference", () => {
const facts = extractFactsFromText("I prefer dark mode in my editor.");
const pref = facts.find((f) => f.category === "preference");
assert.ok(pref, "Should extract a preference fact");
assert.ok(pref.content.toLowerCase().includes("dark mode"));
assert.equal(pref.type, "factual");
});
test("extractFactsFromText: detects 'I like' preference", () => {
const facts = extractFactsFromText("I like TypeScript over JavaScript.");
const pref = facts.find((f) => f.category === "preference");
assert.ok(pref);
assert.ok(pref.content.toLowerCase().includes("typescript"));
});
test("extractFactsFromText: detects 'my favorite is' preference", () => {
const facts = extractFactsFromText("My favorite is VS Code for editing.");
const pref = facts.find((f) => f.category === "preference");
assert.ok(pref);
assert.ok(pref.content.toLowerCase().includes("vs code"));
});
test("extractFactsFromText: detects negative preference (I don't like)", () => {
const facts = extractFactsFromText("I don't like JavaScript callbacks.");
const pref = facts.find((f) => f.category === "preference");
assert.ok(pref);
assert.ok(pref.content.toLowerCase().includes("javascript callbacks"));
});
// ─── extractFactsFromText: Decisions ─────────────────────────────────────────
test("extractFactsFromText: detects 'I'll use' decision", () => {
const facts = extractFactsFromText("I'll use PostgreSQL for this project.");
const dec = facts.find((f) => f.category === "decision");
assert.ok(dec, "Should extract a decision fact");
assert.ok(dec.content.toLowerCase().includes("postgresql"));
assert.equal(dec.type, "episodic");
});
test("extractFactsFromText: detects 'I chose' decision", () => {
const facts = extractFactsFromText("I chose React for the frontend.");
const dec = facts.find((f) => f.category === "decision");
assert.ok(dec);
assert.ok(dec.content.toLowerCase().includes("react"));
});
test("extractFactsFromText: detects 'I decided to' decision", () => {
const facts = extractFactsFromText("I decided to migrate to Docker.");
const dec = facts.find((f) => f.category === "decision");
assert.ok(dec);
assert.ok(dec.content.toLowerCase().includes("migrate to docker"));
});
test("extractFactsFromText: detects 'I went with' decision", () => {
const facts = extractFactsFromText("I went with Tailwind for styling.");
const dec = facts.find((f) => f.category === "decision");
assert.ok(dec);
assert.ok(dec.content.toLowerCase().includes("tailwind"));
});
// ─── extractFactsFromText: Patterns ─────────────────────────────────────────
test("extractFactsFromText: detects 'I usually' pattern", () => {
const facts = extractFactsFromText("I usually start with tests first.");
const pat = facts.find((f) => f.category === "pattern");
assert.ok(pat, "Should extract a pattern fact");
assert.ok(pat.content.toLowerCase().includes("start with tests"));
assert.equal(pat.type, "factual");
});
test("extractFactsFromText: detects 'I always' pattern", () => {
const facts = extractFactsFromText("I always use ESLint in my projects.");
const pat = facts.find((f) => f.category === "pattern");
assert.ok(pat);
assert.ok(pat.content.toLowerCase().includes("eslint"));
});
test("extractFactsFromText: detects 'I never' pattern", () => {
const facts = extractFactsFromText("I never commit directly to main.");
const pat = facts.find((f) => f.category === "pattern");
assert.ok(pat);
assert.ok(pat.content.toLowerCase().includes("commit directly to main"));
});
test("extractFactsFromText: detects 'I tend to' pattern", () => {
const facts = extractFactsFromText("I tend to use functional components.");
const pat = facts.find((f) => f.category === "pattern");
assert.ok(pat);
assert.ok(pat.content.toLowerCase().includes("functional components"));
});
// ─── extractFactsFromText: Multiple facts ───────────────────────────────────
test("extractFactsFromText: extracts multiple facts from one response", () => {
const text =
"I prefer TypeScript. I'll use Next.js for this project. I usually write tests first.";
const facts = extractFactsFromText(text);
assert.ok(facts.length >= 3, `Expected at least 3 facts, got ${facts.length}`);
const categories = facts.map((f) => f.category);
assert.ok(categories.includes("preference"));
assert.ok(categories.includes("decision"));
assert.ok(categories.includes("pattern"));
});
test("extractFactsFromText: deduplicates identical patterns", () => {
const text = "I prefer vim. I prefer vim.";
const facts = extractFactsFromText(text);
const prefs = facts.filter((f) => f.category === "preference" && f.content.includes("vim"));
assert.equal(prefs.length, 1, "Duplicate facts should be deduplicated");
});
// ─── extractFactsFromText: Edge cases ───────────────────────────────────────
test("extractFactsFromText: returns empty array for empty string", () => {
assert.deepEqual(extractFactsFromText(""), []);
});
test("extractFactsFromText: returns empty array for null", () => {
assert.deepEqual(extractFactsFromText(null), []);
});
test("extractFactsFromText: returns empty array for unrelated text", () => {
const facts = extractFactsFromText("The sky is blue. Water is wet. 2 + 2 = 4.");
assert.deepEqual(facts, []);
});
test("extractFactsFromText: produces stable keys", () => {
const facts = extractFactsFromText("I prefer dark mode.");
assert.ok(facts.length > 0);
assert.ok(
facts[0].key.startsWith("preference:"),
`Key should start with category: ${facts[0].key}`
);
});
test("extractFactsFromText: truncates very long matches", () => {
const longContent = "a".repeat(600);
const facts = extractFactsFromText(`I prefer ${longContent}.`);
if (facts.length > 0) {
assert.ok(facts[0].content.length <= 500, "Content should be capped at 500 chars");
}
});
// ─── extractFacts: non-blocking behavior ───────────────────────────────────
test("extractFacts: returns immediately (non-blocking)", () => {
let called = false;
const start = Date.now();
extractFacts("I prefer dark mode.", "key-123", "session-456");
const elapsed = Date.now() - start;
assert.ok(elapsed < 50, `extractFacts should return in <50ms, took ${elapsed}ms`);
});
test("extractFacts: does not throw on empty inputs", () => {
assert.doesNotThrow(() => extractFacts("", "key-123", "session-456"));
assert.doesNotThrow(() => extractFacts("I prefer vim.", "", "session-456"));
assert.doesNotThrow(() => extractFacts("I prefer vim.", "key-123", ""));
assert.doesNotThrow(() => extractFacts(null, "key-123", "session-456"));
});