mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
Two shards on release/v3.8.51 went red in one day with the same signature —
"ENOTEMPTY, Directory not empty: /tmp/omniroute-<test>-XXXXXX" — from
combo-same-provider-cascade (Unit Tests fast-path 4/4, on a PR that touches only
.github/) and auth-policy-embeddings-webfetch-7785 (the 20k-test TIA step). Both pass
alone and on re-run: the cleanup races something still writing into the directory
(SQLite WAL/-shm checkpoint, a worker, the backup) and under a loaded hosted runner
the window opens. 1154 test files do their own cleanup with
fs.rmSync(dir, { recursive: true, force: true }); 57 already asked for retries.
One-shot codemod (scripts/ad-hoc/codemod-rm-maxretries.mjs, kept for the record):
every rm / rmSync / rmdirSync option object with `recursive: true` and no
`maxRetries` gains `maxRetries: 5, retryDelay: 100` — Node itself then retries
ENOTEMPTY/EBUSY/EPERM for up to ~0.5 s before giving up. 2243 call sites in 1292
files under tests/, the shared tests/_setup/isolateDataDir.ts exit hook included.
Only the option object changes: no call site, assertion or import is touched.
Validation: prettier and ESLint (with the frozen suppressions) clean on all 1292
files; a random 20-file sample runs green (quota-redis-store hangs identically on
the untouched tree — it needs a Redis on localhost, an environment matter). The
four unit shards on this PR are the full run.
382 lines
12 KiB
TypeScript
382 lines
12 KiB
TypeScript
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(), "omniroute-memory-store-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const store = await import("../../src/lib/memory/store.ts");
|
|
const { MemoryType } = await import("../../src/lib/memory/types.ts");
|
|
|
|
async function resetStorage() {
|
|
core.resetDbInstance();
|
|
|
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
try {
|
|
if (fs.existsSync(TEST_DATA_DIR)) {
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
break;
|
|
} catch (error: any) {
|
|
if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) {
|
|
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
function insertMemoryRow({
|
|
id,
|
|
apiKeyId = "key-a",
|
|
sessionId = "session-a",
|
|
type = "factual",
|
|
key = "memory:key",
|
|
content = "stored content",
|
|
metadata = "{}",
|
|
createdAt = new Date().toISOString(),
|
|
updatedAt = createdAt,
|
|
expiresAt = null,
|
|
}) {
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
).run(id, apiKeyId, sessionId, type, key, content, metadata, createdAt, updatedAt, expiresAt);
|
|
}
|
|
|
|
/**
|
|
* Drain pending setImmediate callbacks scheduled by store.createMemory/updateMemory
|
|
* (which fire-and-forget the vector upsert). Without this, the test runner may end
|
|
* before the async tasks resolve, causing "asynchronous activity after test ended"
|
|
* errors when the upsert later tries to touch the DB we already tore down.
|
|
*/
|
|
async function drainSetImmediate(rounds = 3): Promise<void> {
|
|
for (let i = 0; i < rounds; i++) {
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
}
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
await resetStorage();
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
// Allow vector upsert fire-and-forget to settle before the next test resets DATA_DIR.
|
|
await drainSetImmediate();
|
|
});
|
|
|
|
test.after(async () => {
|
|
await drainSetImmediate();
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
});
|
|
|
|
test("memory store CRUD round-trip persists to the memories table and invalidates cache on update/delete", async () => {
|
|
const expiresAt = new Date("2026-04-06T00:00:00.000Z");
|
|
const created = await store.createMemory({
|
|
apiKeyId: "key-a",
|
|
sessionId: "session-a",
|
|
type: MemoryType.FACTUAL,
|
|
key: "preference:theme",
|
|
content: "User prefers a compact dashboard",
|
|
metadata: { source: "test" },
|
|
expiresAt,
|
|
});
|
|
|
|
const fetched = await store.getMemory(created.id);
|
|
assert.equal(fetched?.apiKeyId, "key-a");
|
|
assert.equal(fetched?.key, "preference:theme");
|
|
assert.deepEqual(fetched?.metadata, { source: "test" });
|
|
assert.equal(fetched?.expiresAt?.toISOString(), expiresAt.toISOString());
|
|
|
|
const updated = await store.updateMemory(created.id, {
|
|
type: MemoryType.EPISODIC,
|
|
key: "decision:layout",
|
|
content: "Switched to the analytics-first layout",
|
|
metadata: { source: "update" },
|
|
expiresAt: null,
|
|
});
|
|
assert.equal(updated, true);
|
|
|
|
const refreshed = await store.getMemory(created.id);
|
|
assert.equal(refreshed?.type, MemoryType.EPISODIC);
|
|
assert.equal(refreshed?.content, "Switched to the analytics-first layout");
|
|
assert.deepEqual(refreshed?.metadata, { source: "update" });
|
|
assert.equal(refreshed?.expiresAt, null);
|
|
|
|
assert.equal(await store.deleteMemory(created.id), true);
|
|
assert.equal(await store.getMemory(created.id), null);
|
|
assert.equal(await store.deleteMemory(created.id), false);
|
|
});
|
|
|
|
test("getMemory returns null for invalid identifiers and tolerates malformed metadata rows", async () => {
|
|
assert.equal(await store.getMemory(""), null);
|
|
assert.equal(await store.getMemory("missing-id"), null);
|
|
|
|
insertMemoryRow({
|
|
id: "broken-metadata",
|
|
metadata: "{not-json",
|
|
content: "Corrupt metadata should not break reads",
|
|
});
|
|
|
|
const fetched = await store.getMemory("broken-metadata");
|
|
assert.equal(fetched?.content, "Corrupt metadata should not break reads");
|
|
assert.deepEqual(fetched?.metadata, {});
|
|
});
|
|
|
|
test("updateMemory returns false for missing ids and listMemories handles an empty store", async () => {
|
|
assert.equal(await store.updateMemory("missing-id", { content: "noop" }), false);
|
|
const result = await store.listMemories({ apiKeyId: "missing-key" });
|
|
assert.deepEqual(result.data, []);
|
|
assert.equal(result.total, 0);
|
|
});
|
|
|
|
test("listMemories filters by api key, type and session while preserving newest-first ordering", async () => {
|
|
insertMemoryRow({
|
|
id: "mem-1",
|
|
apiKeyId: "key-a",
|
|
sessionId: "session-a",
|
|
type: "factual",
|
|
key: "pref:1",
|
|
content: "Older factual memory",
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "mem-2",
|
|
apiKeyId: "key-a",
|
|
sessionId: "session-a",
|
|
type: "episodic",
|
|
key: "decision:1",
|
|
content: "Newest episodic memory",
|
|
createdAt: "2026-04-03T00:00:00.000Z",
|
|
updatedAt: "2026-04-03T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "mem-3",
|
|
apiKeyId: "key-b",
|
|
sessionId: "session-b",
|
|
type: "semantic",
|
|
key: "semantic:1",
|
|
content: "Different api key",
|
|
createdAt: "2026-04-02T00:00:00.000Z",
|
|
updatedAt: "2026-04-02T00:00:00.000Z",
|
|
});
|
|
|
|
const allForKeyA = await store.listMemories({ apiKeyId: "key-a" });
|
|
const onlySessionA = await store.listMemories({ apiKeyId: "key-a", sessionId: "session-a" });
|
|
const onlyEpisodic = await store.listMemories({ apiKeyId: "key-a", type: MemoryType.EPISODIC });
|
|
|
|
assert.deepEqual(
|
|
allForKeyA.data.map((memory) => memory.id),
|
|
["mem-2", "mem-1"]
|
|
);
|
|
assert.equal(allForKeyA.total, 2);
|
|
assert.deepEqual(
|
|
onlySessionA.data.map((memory) => memory.id),
|
|
["mem-2", "mem-1"]
|
|
);
|
|
assert.equal(onlySessionA.total, 2);
|
|
assert.deepEqual(
|
|
onlyEpisodic.data.map((memory) => memory.id),
|
|
["mem-2"]
|
|
);
|
|
assert.equal(onlyEpisodic.total, 1);
|
|
});
|
|
|
|
test("listMemories supports limit and offset pagination even when only offset is provided", async () => {
|
|
insertMemoryRow({
|
|
id: "page-1",
|
|
key: "pagination:1",
|
|
content: "oldest",
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "page-2",
|
|
key: "pagination:2",
|
|
content: "middle",
|
|
createdAt: "2026-04-02T00:00:00.000Z",
|
|
updatedAt: "2026-04-02T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "page-3",
|
|
key: "pagination:3",
|
|
content: "newest",
|
|
createdAt: "2026-04-03T00:00:00.000Z",
|
|
updatedAt: "2026-04-03T00:00:00.000Z",
|
|
});
|
|
|
|
const paged = await store.listMemories({ apiKeyId: "key-a", limit: 1, offset: 1 });
|
|
const offsetOnly = await store.listMemories({ apiKeyId: "key-a", offset: 1 });
|
|
|
|
assert.deepEqual(
|
|
paged.data.map((memory) => memory.id),
|
|
["page-2"]
|
|
);
|
|
assert.equal(paged.total, 3);
|
|
assert.deepEqual(
|
|
offsetOnly.data.map((memory) => memory.id),
|
|
["page-2", "page-1"]
|
|
);
|
|
assert.equal(offsetOnly.total, 3);
|
|
});
|
|
|
|
test("listMemories applies query filtering before pagination and type stats", async () => {
|
|
insertMemoryRow({
|
|
id: "search-1",
|
|
key: "typescript:guide",
|
|
content: "TypeScript project setup",
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "search-2",
|
|
type: "episodic",
|
|
key: "deployment:note",
|
|
content: "Production deployment checklist",
|
|
createdAt: "2026-04-02T00:00:00.000Z",
|
|
updatedAt: "2026-04-02T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "search-3",
|
|
type: "semantic",
|
|
key: "tooling:typescript",
|
|
content: "Editor plugins and linting",
|
|
createdAt: "2026-04-03T00:00:00.000Z",
|
|
updatedAt: "2026-04-03T00:00:00.000Z",
|
|
});
|
|
|
|
const filtered = await store.listMemories({
|
|
apiKeyId: "key-a",
|
|
query: "typescript",
|
|
limit: 10,
|
|
});
|
|
|
|
assert.deepEqual(
|
|
filtered.data.map((memory) => memory.id),
|
|
["search-3", "search-1"]
|
|
);
|
|
assert.equal(filtered.total, 2);
|
|
assert.deepEqual(filtered.byType, { factual: 1, semantic: 1 });
|
|
});
|
|
|
|
test("listMemories supports page-based pagination (page 1)", async () => {
|
|
insertMemoryRow({
|
|
id: "pg-1",
|
|
key: "page:test:1",
|
|
content: "first",
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "pg-2",
|
|
key: "page:test:2",
|
|
content: "second",
|
|
createdAt: "2026-04-02T00:00:00.000Z",
|
|
updatedAt: "2026-04-02T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "pg-3",
|
|
key: "page:test:3",
|
|
content: "third",
|
|
createdAt: "2026-04-03T00:00:00.000Z",
|
|
updatedAt: "2026-04-03T00:00:00.000Z",
|
|
});
|
|
|
|
const page1 = await store.listMemories({ apiKeyId: "key-a", page: 1, limit: 2 });
|
|
assert.deepEqual(
|
|
page1.data.map((m) => m.id),
|
|
["pg-3", "pg-2"]
|
|
);
|
|
assert.equal(page1.total, 3);
|
|
});
|
|
|
|
test("listMemories supports page-based pagination (page 2 returns remainder)", async () => {
|
|
insertMemoryRow({
|
|
id: "pg-1",
|
|
key: "page:test:1",
|
|
content: "first",
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "pg-2",
|
|
key: "page:test:2",
|
|
content: "second",
|
|
createdAt: "2026-04-02T00:00:00.000Z",
|
|
updatedAt: "2026-04-02T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "pg-3",
|
|
key: "page:test:3",
|
|
content: "third",
|
|
createdAt: "2026-04-03T00:00:00.000Z",
|
|
updatedAt: "2026-04-03T00:00:00.000Z",
|
|
});
|
|
|
|
const page2 = await store.listMemories({ apiKeyId: "key-a", page: 2, limit: 2 });
|
|
assert.deepEqual(
|
|
page2.data.map((m) => m.id),
|
|
["pg-1"]
|
|
);
|
|
assert.equal(page2.total, 3);
|
|
});
|
|
|
|
test("listMemories returns empty data for a page beyond the result set", async () => {
|
|
insertMemoryRow({
|
|
id: "pg-1",
|
|
key: "page:test:1",
|
|
content: "only entry",
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
});
|
|
|
|
const beyondPage = await store.listMemories({ apiKeyId: "key-a", page: 99, limit: 10 });
|
|
assert.deepEqual(beyondPage.data, []);
|
|
assert.equal(beyondPage.total, 1);
|
|
});
|
|
|
|
test("listMemories page parameter defaults to page 1 when omitted with limit", async () => {
|
|
insertMemoryRow({
|
|
id: "pg-1",
|
|
key: "page:test:1",
|
|
content: "first",
|
|
createdAt: "2026-04-01T00:00:00.000Z",
|
|
updatedAt: "2026-04-01T00:00:00.000Z",
|
|
});
|
|
insertMemoryRow({
|
|
id: "pg-2",
|
|
key: "page:test:2",
|
|
content: "second",
|
|
createdAt: "2026-04-02T00:00:00.000Z",
|
|
updatedAt: "2026-04-02T00:00:00.000Z",
|
|
});
|
|
|
|
const defaultPage = await store.listMemories({ apiKeyId: "key-a", limit: 1 });
|
|
assert.deepEqual(
|
|
defaultPage.data.map((m) => m.id),
|
|
["pg-2"]
|
|
);
|
|
assert.equal(defaultPage.total, 2);
|
|
});
|
|
|
|
test("getMemoryTokensUsed sums estimated tokens, optionally scoped to an api key", async () => {
|
|
// Token estimate is floor((LENGTH(content) + 3) / 4) per row (SQLite integer math).
|
|
insertMemoryRow({ id: "tok-1", apiKeyId: "key-a", content: "aaaa" }); // (4+3)/4 = 1
|
|
insertMemoryRow({ id: "tok-2", apiKeyId: "key-b", content: "aaaaaaaa" }); // (8+3)/4 = 2
|
|
|
|
assert.equal(store.getMemoryTokensUsed("key-a"), 1);
|
|
assert.equal(store.getMemoryTokensUsed("key-b"), 2);
|
|
assert.equal(store.getMemoryTokensUsed(), 3); // all memories
|
|
assert.equal(store.getMemoryTokensUsed("missing-key"), 0);
|
|
});
|