Files
nexusAI/test/summarization.test.js
T
2026-08-23 06:57:58 -07:00

80 lines
3.9 KiB
JavaScript

// Session summarization — tests the REAL maybeSummarize decision logic against
// mocked memory + Ollama endpoints. Guards the refactor that moved data-fetching
// into the summarizer (episode-stats + episodes/since) instead of receiving the
// whole session. Constants: THRESHOLD_TOKENS=200, MIN_EPISODES_SINCE=5, MAX_SUMMARY_TOKENS=800.
const { test } = require('node:test');
const assert = require('node:assert');
const { maybeSummarize } = require('../packages/orchestration-service/src/services/summarization');
const json = (obj) => ({ ok: true, status: 200, json: async () => obj });
// Route mocked fetches by URL and record what got written.
function mockMemory({ stats, summaries, since }) {
const calls = { posted: null, patched: null, sinceAfterId: null };
global.fetch = async (url, opts = {}) => {
const u = String(url);
if (u.endsWith('/episode-stats')) return json(stats);
if (u.endsWith('/summaries') && (!opts.method || opts.method === 'GET')) return json(summaries);
if (u.includes('/episodes/since/')) {
calls.sinceAfterId = Number(u.split('/since/').at(-1));
return json(since.filter(ep => ep.id > calls.sinceAfterId));
}
if (u.endsWith('/utility/complete')) return json({ text: 'A concise third-person summary.' });
if (u.endsWith('/summaries') && opts.method === 'POST') { calls.posted = JSON.parse(opts.body); return json({ id: 99 }); }
if (/\/summaries\/\d+$/.test(u) && opts.method === 'PATCH') { calls.patched = JSON.parse(opts.body); return json({ ok: true }); }
throw new Error('unexpected fetch: ' + u);
};
return calls;
}
const eps = (n, startId = 1, tok = 50) =>
Array.from({ length: n }, (_, i) => ({ id: startId + i, user_message: `u${startId + i}`, ai_response: `a${startId + i}`, token_count: tok }));
test('under the token threshold → nothing is written', async () => {
const c = mockMemory({ stats: { totalTokens: 150, count: 3, maxId: 3 }, summaries: [], since: eps(3) });
await maybeSummarize({ id: 1 });
assert.strictEqual(c.posted, null);
assert.strictEqual(c.patched, null);
});
test('over threshold, no prior summary → POST covering all episodes', async () => {
const c = mockMemory({ stats: { totalTokens: 300, count: 6, maxId: 6 }, summaries: [], since: eps(6) });
await maybeSummarize({ id: 2 });
assert.ok(c.posted && !c.patched, 'creates via POST');
assert.strictEqual(c.sinceAfterId, 0, 'fetches since/0 = whole session when no summary exists');
assert.strictEqual(c.posted.episodeRange, '1-6');
assert.strictEqual(c.posted.tokenCount, 300, 'token count comes from the stats aggregate');
});
test('over threshold, small prior summary, ≥5 new → PATCH the tail only', async () => {
const c = mockMemory({
stats: { totalTokens: 550, count: 11, maxId: 11 },
summaries: [{ id: 42, content: 'short summary', episode_range: '1-6' }],
since: eps(11),
});
await maybeSummarize({ id: 3 });
assert.ok(c.patched && !c.posted, 'updates via PATCH');
assert.strictEqual(c.sinceAfterId, 6, 'only fetches the un-summarized tail');
assert.strictEqual(c.patched.episodeRange, '7-11');
});
test('the MIN_EPISODES_SINCE guard blocks re-summarizing too soon', async () => {
const c = mockMemory({
stats: { totalTokens: 450, count: 9, maxId: 9 },
summaries: [{ id: 43, content: 'short', episode_range: '1-6' }],
since: eps(9), // only 3 new (7,8,9) < 5
});
await maybeSummarize({ id: 4 });
assert.strictEqual(c.posted, null);
assert.strictEqual(c.patched, null);
});
test('a large existing summary forces a fresh row instead of appending', async () => {
const c = mockMemory({
stats: { totalTokens: 900, count: 15, maxId: 15 },
summaries: [{ id: 44, content: 'x'.repeat(850), episode_range: '1-6' }], // > MAX_SUMMARY_TOKENS
since: eps(15),
});
await maybeSummarize({ id: 5 });
assert.ok(c.posted && !c.patched, 'large summary → POST fresh row');
});