This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -74,14 +74,116 @@ describe('Hybrid Search (FTS5 + sqlite-vec + RRF + Relevance)', () => {
expect(results).toHaveLength(0);
});
it('falls back to LIKE when an FTS5-special query would parse-error', async () => {
it('recovers when an FTS5-special query would parse-error', async () => {
await seedFrames();
// A lone unbalanced double-quote is passed through verbatim by the
// sanitizer and triggers an FTS5 MATCH parse error. The LIKE fallback
// should still find frames whose content contains the literal substring.
// sanitizer and triggers an FTS5 MATCH parse error. The strict fallback
// should still find frames containing both meaningful terms.
const results = await search.keywordSearch('"Machine learning', 10);
expect(results.length).toBeGreaterThanOrEqual(1);
});
it('recovers one meaningful token after an FTS5 parse error', async () => {
await seedFrames();
const results = await search.keywordSearch('"Machine', 10);
expect(results.length).toBeGreaterThanOrEqual(1);
});
it('falls back when punctuation-delimited identifiers miss the sanitized FTS token', async () => {
const session = sessions.create();
const frame = frames.createIFrame(
session.gop_id,
'Captured roundtrip-debug-abc123 from a hook event',
);
const results = await search.keywordSearch('roundtrip-debug-abc123', 10);
expect(results).toContain(frame.id);
});
it('does not let newer single-token decoys crowd out an exact punctuated identifier', async () => {
const session = sessions.create();
const target = frames.createIFrame(
session.gop_id,
'Captured roundtrip-debug-abc123 from a hook event',
'normal',
'user_stated',
'2026-01-01T00:00:00.000Z',
);
for (let i = 0; i < 25; i += 1) {
frames.createIFrame(
session.gop_id,
`Newer roundtrip decoy ${i}`,
'normal',
'user_stated',
`2026-02-${String(i + 1).padStart(2, '0')}T00:00:00.000Z`,
);
}
const results = await search.keywordSearch('roundtrip-debug-abc123', 10);
expect(results).toContain(target.id);
});
it('keeps punctuation fallback scoped to the requested GOP', async () => {
const first = sessions.create();
const second = sessions.create();
const inScope = frames.createIFrame(
first.gop_id,
'Captured scope-check-xyz789 in the requested session',
);
const outOfScope = frames.createIFrame(
second.gop_id,
'Captured scope-check-xyz789 in another session',
);
const results = await search.keywordSearch('scope-check-xyz789', 10, first.gop_id);
expect(results).toContain(inScope.id);
expect(results).not.toContain(outOfScope.id);
});
it('matches punctuation-delimited Cyrillic identifiers case-insensitively', async () => {
const session = sessions.create();
const frame = frames.createIFrame(
session.gop_id,
'Captured БЕОГРАД-КОНФЕРЕНЦИЈА from an external event',
);
const results = await search.keywordSearch('београд-конференција', 10);
expect(results).toContain(frame.id);
});
it('treats LIKE metacharacters literally in whole-query fallback', async () => {
const session = sessions.create();
const literal = frames.createIFrame(session.gop_id, 'Captured 北京旅行%_\\ marker');
const wildcardDecoy = frames.createIFrame(session.gop_id, 'Captured 北京旅行XXY marker');
const results = await search.keywordSearch('北京旅行%_\\', 10);
expect(results).toContain(literal.id);
expect(results).not.toContain(wildcardDecoy.id);
});
it('does not broaden overlong punctuation fallback queries', async () => {
const session = sessions.create();
const tokens = Array.from({ length: 20 }, (_, i) => `segment${i}`);
const query = tokens.join('-');
const exact = frames.createIFrame(session.gop_id, `Captured ${query} marker`);
const prefixOnly = frames.createIFrame(
session.gop_id,
`Captured ${tokens.slice(0, 16).join('-')} marker`,
);
const results = await search.keywordSearch(query, 10);
expect(results).toContain(exact.id);
expect(results).not.toContain(prefixOnly.id);
});
it('does not broaden punctuation fallback with short or stop-word fragments', async () => {
const session = sessions.create();
const unrelated = frames.createIFrame(session.gop_id, 'Totally unrelated topic');
const results = await search.keywordSearch("doesn't exist", 10);
expect(results).not.toContain(unrelated.id);
});
});
describe('Unicode keyword search (S1)', () => {
@@ -409,6 +511,60 @@ describe('Hybrid Search (FTS5 + sqlite-vec + RRF + Relevance)', () => {
expect(ids).not.toContain(stale.id);
expect(ids).toContain(fresh.id);
});
it('excludes deprecated candidates before keyword and vector lane limits', async () => {
const session = sessions.create();
const query = 'crowdout-token';
const indexed: Array<{ id: number; content: string }> = [];
for (let index = 0; index < 5; index += 1) {
const stale = frames.createIFrame(session.gop_id, `${query} obsolete-${index}`);
indexed.push({ id: stale.id, content: stale.content });
frames.update(stale.id, stale.content, 'deprecated');
}
const live = frames.createIFrame(
session.gop_id,
`${query} live candidate with deliberately lower raw lane similarity`,
);
indexed.push({ id: live.id, content: live.content });
await search.indexFramesBatch(indexed);
await expect(search.keywordSearch(query, 1, undefined, true)).resolves.toEqual([live.id]);
await expect(search.vectorSearch(query, 1, undefined, true)).resolves.toEqual([live.id]);
const hybrid = await search.search(query, { limit: 1, excludeDeprecated: true });
expect(hybrid.map((result) => result.frame.id)).toEqual([live.id]);
const otherSession = sessions.create();
const outOfScopeDecoy = frames.createIFrame(otherSession.gop_id, query);
await search.indexFrame(outOfScopeDecoy.id, outOfScopeDecoy.content);
await expect(search.keywordSearch(query, 1, session.gop_id, true)).resolves.toEqual([live.id]);
await expect(search.vectorSearch(query, 1, session.gop_id, true)).resolves.toEqual([live.id]);
const scopedHybrid = await search.search(query, {
limit: 1,
gopId: session.gop_id,
excludeDeprecated: true,
});
expect(scopedHybrid.map((result) => result.frame.id)).toEqual([live.id]);
});
it('excludes deprecated candidates before the LIKE fallback limit', async () => {
const session = sessions.create();
const live = frames.createIFrame(session.gop_id, '北京旅行 正常记录');
const stale = frames.createIFrame(session.gop_id, '北京旅行 旧记录');
const raw = db.getDatabase();
raw.prepare('UPDATE memory_frames SET created_at = ? WHERE id = ?')
.run('2026-01-01 00:00:00', live.id);
raw.prepare('UPDATE memory_frames SET created_at = ? WHERE id = ?')
.run('2026-02-01 00:00:00', stale.id);
frames.update(stale.id, stale.content, 'deprecated');
const otherSession = sessions.create();
const outOfScopeDecoy = frames.createIFrame(otherSession.gop_id, '北京旅行');
raw.prepare('UPDATE memory_frames SET created_at = ? WHERE id = ?')
.run('2026-03-01 00:00:00', outOfScopeDecoy.id);
await expect(search.keywordSearch('北京旅行', 1, undefined, true)).resolves.toEqual([outOfScopeDecoy.id]);
await expect(search.keywordSearch('北京旅行', 1, session.gop_id, true)).resolves.toEqual([live.id]);
});
});
function getTopicContent(i: number): string {