doatlas-2 / artifacts /api-server /src /lib /__tests__ /memory.unit.test.mjs
Iostream-Li's picture
Add files using upload-large-folder tool
5871090 verified
// Pure unit tests for lib/memory.ts. These import the module directly
// (Node 24 strips TypeScript types natively) and exercise the helpers
// that have no database dependencies: normalisation/dedupe keys, the
// token estimator, the hard-cap constants, the kind whitelist, and the
// pure ranker (`rankAndPackFacts`) — including header+instruction
// overhead and the per-turn token budget.
//
// Run with the api-server's DATABASE_URL exported (the import path
// constructs a pg Pool at module load, but does not actually connect):
// node --test artifacts/api-server/src/lib/__tests__/memory.unit.test.mjs
import { test } from "node:test";
import assert from "node:assert/strict";
import {
MEMORY_HARD_CAPS,
MEMORY_HEADER,
MEMORY_INSTRUCTION_USE,
MEMORY_INSTRUCTION_EXTRACT,
MEMORY_BOOTSTRAP_EXTRACT,
VALID_KINDS,
contentHash,
estimateTokens,
normalize,
rankAndPackFacts,
} from "../memory-core.ts";
// ---------- helpers
function fact(overrides = {}) {
const now = new Date().toISOString();
return {
id: overrides.id || `memf_${Math.random().toString(36).slice(2, 10)}`,
kind: "fact",
content: "placeholder content",
confidence: 0.7,
salience: 0.6,
source: "auto",
source_message_id: null,
conversation_id: null,
use_count: 0,
archived: false,
created_at: now,
updated_at: now,
last_used_at: null,
...overrides,
};
}
const cfgOn = {
enabled: true,
auto_extract: true,
max_facts: 100,
max_tokens_per_turn: 2000,
};
// ---------- hard caps
test("hard caps are pinned to 100 facts / 2000 tokens / 5 new per turn", () => {
assert.equal(MEMORY_HARD_CAPS.facts, 100);
assert.equal(MEMORY_HARD_CAPS.tokensPerTurn, 2000);
assert.equal(MEMORY_HARD_CAPS.newPerTurn, 5);
assert.equal(MEMORY_HARD_CAPS.contentChars, 500);
});
test("VALID_KINDS lists exactly the six spec kinds", () => {
assert.deepEqual(
[...VALID_KINDS].sort(),
["domain", "fact", "interest", "preference", "summary", "terminology"],
);
});
// ---------- normalize + dedupe key
test("normalize collapses case, punctuation and whitespace", () => {
assert.equal(
normalize(" User PREFERS concise, bullet-point answers!! "),
"user prefers concise bullet point answers",
);
});
test("normalize handles unicode punctuation and CJK content", () => {
assert.equal(normalize("用户偏好:简洁的、要点式回答。"), "用户偏好 简洁的 要点式回答");
});
test("normalize-based dedupe: cosmetic variants share the same content hash", () => {
const a = "User's lab focuses on KRAS G12C in NSCLC.";
const b = "user s lab focuses on kras g12c in nsclc";
const c = "USER'S lab focuses on KRAS-G12C, in NSCLC!!!";
assert.equal(normalize(a), normalize(b));
assert.equal(normalize(a), normalize(c));
assert.equal(contentHash(a), contentHash(b));
assert.equal(contentHash(a), contentHash(c));
});
test("normalize-based dedupe: substantive differences do not collide", () => {
assert.notEqual(
contentHash("User likes mouse models"),
contentHash("User dislikes mouse models"),
);
assert.notEqual(contentHash(""), contentHash("anything"));
});
// ---------- token estimator
test("estimateTokens returns 0 for empty input and ceils to ~chars/4 otherwise", () => {
assert.equal(estimateTokens(""), 0);
assert.equal(estimateTokens("a"), 1);
assert.equal(estimateTokens("abcd"), 1);
assert.equal(estimateTokens("abcde"), 2);
assert.equal(estimateTokens("x".repeat(400)), 100);
});
// ---------- ranker scoring
test("rankAndPackFacts returns nothing when memory is disabled", () => {
const out = rankAndPackFacts([fact()], "anything", { ...cfgOn, enabled: false });
assert.deepEqual(out, { facts: [], fact_ids: [], injected_tokens: 0, text: null });
});
test("rankAndPackFacts returns the bootstrap extract when store is empty + auto_extract on", () => {
const out = rankAndPackFacts([], "hello", cfgOn);
assert.equal(out.text, MEMORY_BOOTSTRAP_EXTRACT);
assert.equal(out.injected_tokens, estimateTokens(MEMORY_BOOTSTRAP_EXTRACT));
assert.deepEqual(out.facts, []);
assert.deepEqual(out.fact_ids, []);
});
test("rankAndPackFacts returns null text when store is empty and auto_extract is off", () => {
const out = rankAndPackFacts([], "hello", { ...cfgOn, auto_extract: false });
assert.equal(out.text, null);
assert.equal(out.injected_tokens, 0);
});
test("rankAndPackFacts ranks higher salience first when query has no overlap", () => {
const high = fact({ id: "hi", salience: 0.95, content: "alpha alpha alpha" });
const low = fact({ id: "lo", salience: 0.2, content: "beta beta beta" });
const out = rankAndPackFacts([low, high], "unrelated query gamma", cfgOn);
assert.deepEqual(out.fact_ids, ["hi", "lo"]);
});
test("rankAndPackFacts boosts facts whose tokens overlap the query", () => {
// Without keyword overlap, baseline would tie on salience. Add an
// overlap term and watch the relevant fact jump to first place.
const relevant = fact({
id: "rel",
salience: 0.5,
content: "User is researching KRAS G12C inhibitors in lung cancer",
});
const irrelevant = fact({
id: "irr",
salience: 0.5,
content: "User likes pour-over coffee in the morning",
});
const out = rankAndPackFacts(
[irrelevant, relevant],
"any updates on KRAS inhibitors?",
cfgOn,
);
assert.equal(out.fact_ids[0], "rel", "keyword overlap must outrank irrelevant tie");
assert.ok(out.fact_ids.includes("irr"));
});
test("rankAndPackFacts builds the system-prompt block with header + facts + instruction", () => {
const f1 = fact({ id: "a", kind: "preference", content: "Prefers concise answers", salience: 0.9 });
const f2 = fact({ id: "b", kind: "fact", content: "PI at Stanford lab", salience: 0.8 });
const out = rankAndPackFacts([f1, f2], "hello", cfgOn);
assert.ok(out.text?.startsWith(MEMORY_HEADER), "must lead with header");
assert.ok(out.text?.includes("- [preference] Prefers concise answers"));
assert.ok(out.text?.includes("- [fact] PI at Stanford lab"));
assert.ok(
out.text?.endsWith(MEMORY_INSTRUCTION_USE + MEMORY_INSTRUCTION_EXTRACT),
"must end with use+extract instruction when auto_extract is on",
);
});
test("rankAndPackFacts omits the extract instruction when auto_extract is off", () => {
const f1 = fact({ id: "a", content: "Anything", salience: 0.9 });
const out = rankAndPackFacts([f1], "hello", { ...cfgOn, auto_extract: false });
assert.ok(out.text?.endsWith(MEMORY_INSTRUCTION_USE));
assert.ok(!out.text?.includes(MEMORY_INSTRUCTION_EXTRACT));
});
// ---------- token-budget cap (header + instruction overhead included)
test("rankAndPackFacts injected_tokens accounts for header + instruction overhead", () => {
const overheadOn =
estimateTokens(MEMORY_HEADER) +
estimateTokens(MEMORY_INSTRUCTION_USE + MEMORY_INSTRUCTION_EXTRACT);
const overheadOff =
estimateTokens(MEMORY_HEADER) + estimateTokens(MEMORY_INSTRUCTION_USE);
assert.ok(overheadOn > overheadOff, "extract instruction must add overhead");
const f = fact({ id: "x", kind: "fact", content: "hello world", salience: 0.9 });
const lineCost = estimateTokens(`- [${f.kind}] ${f.content}\n`);
const onOut = rankAndPackFacts([f], "q", cfgOn);
assert.equal(
onOut.injected_tokens,
overheadOn + lineCost,
"auto_extract on: tokens = header + use+extract instruction + line",
);
const offOut = rankAndPackFacts([f], "q", { ...cfgOn, auto_extract: false });
assert.equal(
offOut.injected_tokens,
overheadOff + lineCost,
"auto_extract off: tokens = header + use instruction + line",
);
});
test("rankAndPackFacts respects the token budget — drops facts that don't fit", () => {
// Build many fat facts whose combined size obviously exceeds the cap,
// then confirm only as many as fit are included and injected_tokens
// never exceeds the budget.
const many = [];
for (let i = 0; i < 60; i++) {
many.push(fact({
id: `f${i}`,
kind: "fact",
content: `Fact #${i}: ${"x".repeat(180)}`,
salience: 0.5 + (i % 5) * 0.05,
}));
}
const tightCfg = { ...cfgOn, max_tokens_per_turn: 400 };
const out = rankAndPackFacts(many, "anything", tightCfg);
assert.ok(out.facts.length > 0, "should fit at least one fact");
assert.ok(out.facts.length < many.length, "must not include them all under tight cap");
assert.ok(out.injected_tokens <= 400, `tokens ${out.injected_tokens} must respect budget`);
});
test("rankAndPackFacts caps the budget at MEMORY_HARD_CAPS.tokensPerTurn even if cfg asks more", () => {
// Even if a misconfigured caller passes a huge budget, the packer
// must clamp to the hard cap of 2000.
const many = [];
for (let i = 0; i < 200; i++) {
many.push(fact({
id: `f${i}`,
content: `Fact #${i}: ${"y".repeat(400)}`,
salience: 0.5,
}));
}
const out = rankAndPackFacts(many, "anything", {
...cfgOn,
max_tokens_per_turn: 999_999,
});
assert.ok(
out.injected_tokens <= MEMORY_HARD_CAPS.tokensPerTurn,
`tokens ${out.injected_tokens} must stay <= 2000 even when cfg over-asks`,
);
});
test("rankAndPackFacts caps picked count at 50 even if budget allows more", () => {
// Tiny content so all 200 would fit token-wise; the loop's hard cap
// of 50 picked rows must still trigger.
const many = [];
for (let i = 0; i < 200; i++) {
many.push(fact({ id: `f${i}`, content: `t${i}`, salience: 0.5 }));
}
const out = rankAndPackFacts(many, "q", cfgOn);
assert.equal(out.facts.length, 50);
assert.equal(out.fact_ids.length, 50);
});