2026-04-17 12:56:41 -04:00
import { beforeEach , describe , expect , it , vi } from 'vitest'
const allMock = vi . fn ()
2026-04-26 10:44:51 +08:00
const indexAllMock = vi . fn ()
2026-04-22 14:00:34 +08:00
const titleAllMock = vi . fn ()
const contentAllMock = vi . fn ()
const likeAllMock = vi . fn ()
const prepareMock = vi . fn (( sql : string ) => {
if ( sql . includes ( 'messages_fts MATCH' )) return ({ all : contentAllMock })
2026-04-24 20:41:14 +08:00
if ( sql . includes ( 'JOIN messages m' ) && sql . includes ( 'LIKE' )) return ({ all : likeAllMock })
if ( sql . includes ( 'base.title' ) && sql . includes ( 'LIKE' )) return ({ all : titleAllMock })
2026-04-26 10:44:51 +08:00
// loadAllSessions: full table scan — contains parent_session_id but NOT base/CTE/WHERE
if ( sql . includes ( 'parent_session_id AS parent_session_id' ) && ! sql . includes ( 'base' ) && ! sql . includes ( 'parent_session_id IS NULL' )) return ({ all : indexAllMock })
2026-04-22 14:00:34 +08:00
return ({ all : allMock })
})
2026-04-17 12:56:41 -04:00
const closeMock = vi . fn ()
const databaseSyncMock = vi . fn (() => ({ prepare : prepareMock , close : closeMock }))
const getActiveProfileDirMock = vi . fn (() => '/tmp/hermes-profile' )
2026-04-18 09:34:59 +08:00
vi . doMock ( 'node:sqlite' , () => ({
2026-04-17 12:56:41 -04:00
DatabaseSync : databaseSyncMock ,
}))
vi . mock ( '../../packages/server/src/services/hermes/hermes-profile' , () => ({
getActiveProfileDir : getActiveProfileDirMock ,
}))
describe ( 'session DB summaries' , () => {
beforeEach (() => {
vi . resetModules ()
allMock . mockReset ()
2026-04-26 10:44:51 +08:00
indexAllMock . mockReset ()
indexAllMock . mockReturnValue ([])
2026-04-22 14:00:34 +08:00
titleAllMock . mockReset ()
contentAllMock . mockReset ()
likeAllMock . mockReset ()
2026-04-17 12:56:41 -04:00
prepareMock . mockClear ()
closeMock . mockClear ()
databaseSyncMock . mockClear ()
getActiveProfileDirMock . mockReset ()
getActiveProfileDirMock . mockReturnValue ( '/tmp/hermes-profile' )
})
it ( 'queries sqlite for lightweight session summaries' , async () => {
allMock . mockReturnValue ([
{
id : 's1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : 'Named session' ,
started_at : 1710000000 ,
ended_at : null ,
end_reason : '' ,
message_count : 3 ,
tool_call_count : 1 ,
input_tokens : 10 ,
output_tokens : 20 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : 'openrouter' ,
estimated_cost_usd : 0.01 ,
actual_cost_usd : null ,
cost_status : 'estimated' ,
preview : 'hello world' ,
last_active : 1710000005 ,
},
])
2026-04-22 16:14:50 +08:00
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-17 12:56:41 -04:00
const rows = await mod . listSessionSummaries ( undefined , 50 )
expect ( databaseSyncMock ). toHaveBeenCalledWith ( '/tmp/hermes-profile/state.db' , { open : true , readOnly : true })
2026-04-26 04:10:01 +02:00
expect ( prepareMock ). toHaveBeenCalledWith ( expect . stringContaining ( "s.source != 'tool'" ))
expect ( allMock ). toHaveBeenCalledWith ( 200 )
2026-04-17 12:56:41 -04:00
expect ( closeMock ). toHaveBeenCalled ()
expect ( rows ). toEqual ([
{
id : 's1' ,
source : 'cli' ,
user_id : null ,
model : 'openai/gpt-5.4' ,
title : 'Named session' ,
started_at : 1710000000 ,
ended_at : null ,
end_reason : null ,
message_count : 3 ,
tool_call_count : 1 ,
input_tokens : 10 ,
output_tokens : 20 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : 'openrouter' ,
estimated_cost_usd : 0.01 ,
actual_cost_usd : null ,
cost_status : 'estimated' ,
preview : 'hello world' ,
last_active : 1710000005 ,
},
])
})
it ( 'adds source filter and falls back last_active to started_at' , async () => {
allMock . mockReturnValue ([
{
id : 's2' ,
source : 'telegram' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : '' ,
started_at : 1710000100 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 4 ,
output_tokens : 5 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'preview text' ,
last_active : null ,
},
])
2026-04-22 16:14:50 +08:00
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-17 12:56:41 -04:00
const rows = await mod . listSessionSummaries ( 'telegram' , 2 )
2026-04-26 04:10:01 +02:00
expect ( prepareMock ). toHaveBeenCalledWith ( expect . stringContaining ( "s.source != 'tool'" ))
expect ( allMock ). toHaveBeenCalledWith ( 'telegram' , 8 )
2026-04-17 12:56:41 -04:00
expect ( rows [ 0 ]. last_active ). toBe ( 1710000100 )
expect ( rows [ 0 ]. source ). toBe ( 'telegram' )
2026-04-18 08:53:45 +08:00
expect ( rows [ 0 ]. title ). toBe ( 'preview text' )
2026-04-17 12:56:41 -04:00
})
2026-04-22 14:00:34 +08:00
it ( 'searches session titles and content with deduped results' , async () => {
titleAllMock . mockReturnValue ([
{
id : 'title-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : 'Docker debugging' ,
started_at : 1710001000 ,
ended_at : null ,
end_reason : '' ,
message_count : 2 ,
tool_call_count : 0 ,
input_tokens : 1 ,
output_tokens : 2 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'title preview' ,
last_active : 1710001005 ,
matched_message_id : null ,
snippet : 'Docker debugging' ,
rank : 0 ,
},
])
contentAllMock . mockReturnValue ([
{
id : 'title-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : 'Docker debugging' ,
started_at : 1710001000 ,
ended_at : null ,
end_reason : '' ,
message_count : 2 ,
tool_call_count : 0 ,
input_tokens : 1 ,
output_tokens : 2 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'title preview' ,
last_active : 1710001005 ,
matched_message_id : 42 ,
snippet : '>>>docker<<< compose up' ,
rank : 0.25 ,
},
{
id : 'content-2' ,
source : 'telegram' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : '' ,
started_at : 1710002000 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 3 ,
output_tokens : 4 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'content preview' ,
last_active : 1710002001 ,
matched_message_id : 7 ,
snippet : '>>>docker<<< swarm' ,
rank : 0.1 ,
},
])
2026-04-22 16:14:50 +08:00
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-22 14:00:34 +08:00
const rows = await mod . searchSessionSummaries ( 'docker' , undefined , 10 )
expect ( prepareMock ). toHaveBeenCalledWith ( expect . stringContaining ( 'messages_fts MATCH' ))
expect ( rows ). toHaveLength ( 2 )
expect ( rows [ 0 ]. id ). toBe ( 'title-1' )
expect ( rows [ 0 ]. matched_message_id ). toBeNull ()
expect ( rows [ 0 ]. snippet ). toBe ( 'Docker debugging' )
expect ( rows [ 1 ]. id ). toBe ( 'content-2' )
expect ( rows [ 1 ]. matched_message_id ). toBe ( 7 )
expect ( rows [ 1 ]. snippet ). toContain ( 'docker' )
})
2026-04-24 20:41:14 +08:00
it ( 'falls back to literal content search for punctuation-only queries instead of unsafe FTS' , async () => {
2026-04-23 04:49:00 +02:00
titleAllMock . mockReturnValue ([])
contentAllMock . mockImplementation (() => {
2026-04-24 20:41:14 +08:00
throw new Error ( 'fts5: syntax error near "."' )
2026-04-23 04:49:00 +02:00
})
likeAllMock . mockReturnValue ([
{
2026-04-24 20:41:14 +08:00
id : 'dot-1' ,
2026-04-23 04:49:00 +02:00
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : '' ,
2026-04-24 20:41:14 +08:00
started_at : 1710004000 ,
2026-04-23 04:49:00 +02:00
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
2026-04-24 20:41:14 +08:00
input_tokens : 1 ,
output_tokens : 1 ,
2026-04-23 04:49:00 +02:00
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
2026-04-24 20:41:14 +08:00
preview : 'punctuation preview' ,
last_active : 1710004001 ,
matched_message_id : 21 ,
snippet : 'value.with.dot' ,
2026-04-23 04:49:00 +02:00
rank : 0 ,
},
])
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-24 20:41:14 +08:00
const rows = await mod . searchSessionSummaries ( '.' , undefined , 10 )
2026-04-23 04:49:00 +02:00
2026-04-24 20:41:14 +08:00
expect ( contentAllMock ). not . toHaveBeenCalled ()
expect ( likeAllMock ). toHaveBeenCalled ()
2026-04-23 04:49:00 +02:00
expect ( rows ). toHaveLength ( 1 )
2026-04-24 20:41:14 +08:00
expect ( rows [ 0 ]. id ). toBe ( 'dot-1' )
2026-04-23 04:49:00 +02:00
})
2026-04-24 20:41:14 +08:00
it ( 'keeps safe dotted queries on the FTS path' , async () => {
2026-04-23 04:49:00 +02:00
titleAllMock . mockReturnValue ([])
2026-04-24 20:41:14 +08:00
contentAllMock . mockReturnValue ([
2026-04-23 04:49:00 +02:00
{
2026-04-24 20:41:14 +08:00
id : 'node-1' ,
source : 'cli' ,
2026-04-23 04:49:00 +02:00
user_id : '' ,
model : 'openai/gpt-5.4' ,
2026-04-24 20:41:14 +08:00
title : 'Node.js notes' ,
started_at : 1710004500 ,
2026-04-23 04:49:00 +02:00
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
2026-04-24 20:41:14 +08:00
input_tokens : 1 ,
output_tokens : 1 ,
2026-04-23 04:49:00 +02:00
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
2026-04-24 20:41:14 +08:00
preview : 'dotted preview' ,
last_active : 1710004501 ,
matched_message_id : 22 ,
snippet : '>>>node.js<<< runtime' ,
rank : 0.2 ,
2026-04-23 04:49:00 +02:00
},
])
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-24 20:41:14 +08:00
const rows = await mod . searchSessionSummaries ( 'node.js' , undefined , 10 )
2026-04-23 04:49:00 +02:00
2026-04-24 20:41:14 +08:00
expect ( contentAllMock ). toHaveBeenCalled ()
expect ( likeAllMock ). not . toHaveBeenCalled ()
2026-04-23 04:49:00 +02:00
expect ( rows ). toHaveLength ( 1 )
2026-04-24 20:41:14 +08:00
expect ( rows [ 0 ]. id ). toBe ( 'node-1' )
2026-04-23 04:49:00 +02:00
})
2026-04-24 20:41:14 +08:00
it ( 'keeps explicit wildcard dotted queries on the FTS path with valid syntax' , async () => {
2026-04-23 04:49:00 +02:00
titleAllMock . mockReturnValue ([
{
2026-04-24 20:41:14 +08:00
id : 'node-wildcard-title-1' ,
2026-04-23 04:49:00 +02:00
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
2026-04-24 20:41:14 +08:00
title : 'Node.js wildcard notes' ,
started_at : 1710004590 ,
2026-04-23 04:49:00 +02:00
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
2026-04-24 20:41:14 +08:00
input_tokens : 1 ,
output_tokens : 1 ,
2026-04-23 04:49:00 +02:00
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
2026-04-24 20:41:14 +08:00
preview : 'wildcard title preview' ,
last_active : 1710004595 ,
2026-04-23 04:49:00 +02:00
matched_message_id : null ,
2026-04-24 20:41:14 +08:00
snippet : 'Node.js wildcard notes' ,
2026-04-23 04:49:00 +02:00
rank : 0 ,
},
])
2026-04-24 20:41:14 +08:00
contentAllMock . mockReturnValue ([
2026-04-23 04:49:00 +02:00
{
2026-04-24 20:41:14 +08:00
id : 'node-wildcard-1' ,
2026-04-23 04:49:00 +02:00
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
2026-04-24 20:41:14 +08:00
title : 'Node.js wildcard notes' ,
started_at : 1710004600 ,
2026-04-23 04:49:00 +02:00
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
2026-04-24 20:41:14 +08:00
input_tokens : 1 ,
output_tokens : 1 ,
2026-04-23 04:49:00 +02:00
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
2026-04-24 20:41:14 +08:00
preview : 'wildcard dotted preview' ,
last_active : 1710004601 ,
matched_message_id : 24 ,
snippet : '>>>node.js<<< runtime' ,
rank : 0.15 ,
},
])
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
const rows = await mod . searchSessionSummaries ( 'node.js*' , undefined , 10 )
2026-04-26 04:10:01 +02:00
expect ( titleAllMock ). toHaveBeenCalledWith ( '%node.js%' , 200 )
expect ( contentAllMock ). toHaveBeenCalledWith ( '"node.js"*' , 200 )
2026-04-24 20:41:14 +08:00
expect ( likeAllMock ). not . toHaveBeenCalled ()
expect ( rows ). toHaveLength ( 2 )
expect ( rows [ 0 ]. id ). toBe ( 'node-wildcard-title-1' )
expect ( rows [ 1 ]. id ). toBe ( 'node-wildcard-1' )
})
it ( 'keeps quoted wildcard dotted queries on the FTS path with valid syntax' , async () => {
titleAllMock . mockReturnValue ([
{
id : 'node-quoted-title-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : 'Quoted Node.js wildcard notes' ,
started_at : 1710004640 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 1 ,
output_tokens : 1 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'quoted title preview' ,
last_active : 1710004645 ,
matched_message_id : null ,
snippet : 'Quoted Node.js wildcard notes' ,
rank : 0 ,
},
])
contentAllMock . mockReturnValue ([
{
id : 'node-quoted-wildcard-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : 'Quoted Node.js wildcard notes' ,
started_at : 1710004650 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 1 ,
output_tokens : 1 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'quoted wildcard dotted preview' ,
last_active : 1710004651 ,
matched_message_id : 25 ,
snippet : '>>>node.js<<< runtime' ,
rank : 0.12 ,
},
])
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
const rows = await mod . searchSessionSummaries ( '"node.js"*' , undefined , 10 )
2026-04-26 04:10:01 +02:00
expect ( titleAllMock ). toHaveBeenCalledWith ( '%node.js%' , 200 )
expect ( contentAllMock ). toHaveBeenCalledWith ( '"node.js"*' , 200 )
2026-04-24 20:41:14 +08:00
expect ( likeAllMock ). not . toHaveBeenCalled ()
expect ( rows ). toHaveLength ( 2 )
expect ( rows [ 0 ]. id ). toBe ( 'node-quoted-title-1' )
expect ( rows [ 1 ]. id ). toBe ( 'node-quoted-wildcard-1' )
})
it ( 'keeps non-ASCII dotted queries on the safe quoted FTS path' , async () => {
titleAllMock . mockReturnValue ([])
contentAllMock . mockReturnValue ([
{
id : 'unicode-dot-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : 'naïve.js note' ,
started_at : 1710004700 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 1 ,
output_tokens : 1 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'unicode dotted preview' ,
last_active : 1710004701 ,
matched_message_id : 23 ,
snippet : 'naïve.js runtime' ,
2026-04-23 04:49:00 +02:00
rank : 0 ,
},
])
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-24 20:41:14 +08:00
const rows = await mod . searchSessionSummaries ( 'naïve.js' , undefined , 10 )
2026-04-23 04:49:00 +02:00
2026-04-26 04:10:01 +02:00
expect ( contentAllMock ). toHaveBeenCalledWith ( '"naïve.js"' , 200 )
2026-04-24 20:41:14 +08:00
expect ( likeAllMock ). not . toHaveBeenCalled ()
expect ( rows ). toHaveLength ( 1 )
expect ( rows [ 0 ]. id ). toBe ( 'unicode-dot-1' )
2026-04-23 04:49:00 +02:00
})
2026-04-24 20:41:14 +08:00
it ( 'escapes LIKE wildcards for literal special-character searches' , async () => {
2026-04-22 14:00:34 +08:00
titleAllMock . mockReturnValue ([])
2026-04-24 20:41:14 +08:00
likeAllMock . mockReturnValue ([
{
id : 'percent-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : '100% reproducible' ,
started_at : 1710005000 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 1 ,
output_tokens : 1 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'literal percent preview' ,
last_active : 1710005001 ,
matched_message_id : 31 ,
snippet : '100% reproducible' ,
rank : 0 ,
},
])
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
const rows = await mod . searchSessionSummaries ( '100%' , undefined , 10 )
2026-04-26 04:10:01 +02:00
expect ( titleAllMock ). toHaveBeenCalledWith ( '%100\\%%' , 200 )
2026-04-24 20:41:14 +08:00
expect ( rows ). toHaveLength ( 1 )
expect ( rows [ 0 ]. id ). toBe ( 'percent-1' )
})
it ( 'uses literal search for CJK queries even when FTS returns no rows' , async () => {
titleAllMock . mockReturnValue ([])
contentAllMock . mockReturnValue ([])
likeAllMock . mockReturnValue ([
{
id : 'cjk-literal-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : '' ,
started_at : 1710002980 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 2 ,
output_tokens : 3 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : '中文内容预览' ,
last_active : 1710002985 ,
matched_message_id : 10 ,
snippet : '这里也有记忆断裂' ,
rank : 0 ,
},
])
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
const rows = await mod . searchSessionSummaries ( '记忆断裂' , undefined , 10 )
expect ( contentAllMock ). not . toHaveBeenCalled ()
2026-04-26 04:10:01 +02:00
expect ( likeAllMock ). toHaveBeenCalledWith ( '记忆断裂' , '%记忆断裂%' , 200 )
2026-04-24 20:41:14 +08:00
expect ( rows ). toHaveLength ( 1 )
expect ( rows [ 0 ]. id ). toBe ( 'cjk-literal-1' )
})
it ( 'falls back to LIKE search for CJK queries while preserving title matches' , async () => {
titleAllMock . mockReturnValue ([
{
id : 'cjk-title-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : '记忆断裂标题' ,
started_at : 1710002990 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 2 ,
output_tokens : 2 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'title preview' ,
last_active : 1710002995 ,
matched_message_id : null ,
snippet : '记忆断裂标题' ,
rank : 0 ,
},
])
2026-04-22 14:00:34 +08:00
contentAllMock . mockImplementation (() => {
throw new Error ( 'fts5 tokenizer miss' )
})
likeAllMock . mockReturnValue ([
{
id : 'cjk-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : '' ,
started_at : 1710003000 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 3 ,
output_tokens : 4 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : '中文预览' ,
last_active : 1710003002 ,
matched_message_id : 11 ,
snippet : '这是一段记忆断裂的内容' ,
rank : 0 ,
},
])
2026-04-22 16:14:50 +08:00
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-22 14:00:34 +08:00
const rows = await mod . searchSessionSummaries ( '记忆断裂' , undefined , 10 )
2026-04-26 04:10:01 +02:00
expect ( likeAllMock ). toHaveBeenCalledWith ( '记忆断裂' , '%记忆断裂%' , 200 )
2026-04-24 20:41:14 +08:00
expect ( rows ). toHaveLength ( 2 )
2026-04-22 14:00:34 +08:00
expect ( rows [ 0 ]. id ). toBe ( 'cjk-1' )
2026-04-24 20:41:14 +08:00
expect ( rows [ 1 ]. id ). toBe ( 'cjk-title-1' )
2026-04-22 14:00:34 +08:00
expect ( rows [ 0 ]. snippet ). toContain ( '记忆断裂' )
})
2026-04-23 04:49:00 +02:00
2026-04-26 10:44:51 +08:00
it ( 'falls back to title results when FTS content query fails' , async () => {
2026-04-24 20:41:14 +08:00
titleAllMock . mockReturnValue ([])
contentAllMock . mockImplementation (() => {
throw new Error ( 'database malformed' )
})
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-26 10:44:51 +08:00
const rows = await mod . searchSessionSummaries ( 'docker' , undefined , 10 )
expect ( rows ). toEqual ([])
2026-04-24 20:41:14 +08:00
expect ( likeAllMock ). not . toHaveBeenCalled ()
})
2026-04-26 10:44:51 +08:00
it ( 'falls back to title results for numeric queries when FTS fails' , async () => {
2026-04-24 20:41:14 +08:00
titleAllMock . mockReturnValue ([])
contentAllMock . mockImplementation (() => {
throw new Error ( 'no such table: messages_fts' )
})
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-26 10:44:51 +08:00
const rows = await mod . searchSessionSummaries ( '123' , undefined , 10 )
expect ( rows ). toEqual ([])
2026-04-24 20:41:14 +08:00
expect ( likeAllMock ). not . toHaveBeenCalled ()
})
2026-04-26 10:44:51 +08:00
it ( 'falls back to title results for numeric queries with source filter when FTS fails' , async () => {
2026-04-24 20:41:14 +08:00
titleAllMock . mockReturnValue ([])
contentAllMock . mockImplementation (() => {
throw new Error ( 'no such table: messages_fts' )
})
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-26 10:44:51 +08:00
const rows = await mod . searchSessionSummaries ( '123' , 'telegram' , 10 )
expect ( rows ). toEqual ([])
2026-04-24 20:41:14 +08:00
})
2026-04-26 10:44:51 +08:00
it ( 'returns title matches for numeric queries even when content search fails' , async () => {
2026-04-24 20:41:14 +08:00
titleAllMock . mockReturnValue ([
{
id : 'title-123' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : 'Issue 123' ,
started_at : 1710002900 ,
ended_at : null ,
end_reason : '' ,
message_count : 1 ,
tool_call_count : 0 ,
input_tokens : 2 ,
output_tokens : 3 ,
cache_read_tokens : 0 ,
cache_write_tokens : 0 ,
reasoning_tokens : 0 ,
billing_provider : '' ,
estimated_cost_usd : 0 ,
actual_cost_usd : null ,
cost_status : '' ,
preview : 'title numeric preview' ,
last_active : 1710002910 ,
matched_message_id : null ,
snippet : 'Issue 123' ,
rank : 0 ,
},
])
contentAllMock . mockImplementation (() => {
throw new Error ( 'no such table: messages_fts' )
})
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-26 10:44:51 +08:00
const rows = await mod . searchSessionSummaries ( '123' , undefined , 10 )
expect ( rows ). toHaveLength ( 1 )
expect ( rows [ 0 ]. id ). toBe ( 'title-123' )
expect ( rows [ 0 ]. title ). toBe ( 'Issue 123' )
2026-04-24 20:41:14 +08:00
})
2026-04-26 10:44:51 +08:00
it ( 'falls back to title results for non-numeric queries when FTS fails' , async () => {
2026-04-23 04:49:00 +02:00
titleAllMock . mockReturnValue ([])
contentAllMock . mockImplementation (() => {
throw new Error ( 'no such table: messages_fts' )
})
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-26 10:44:51 +08:00
const rows = await mod . searchSessionSummaries ( 'docker' , undefined , 10 )
expect ( rows ). toEqual ([])
2026-04-23 04:49:00 +02:00
})
2026-04-26 10:44:51 +08:00
it ( 'falls back to title results for any query when FTS has unrelated database failure' , async () => {
2026-04-23 04:49:00 +02:00
titleAllMock . mockReturnValue ([])
contentAllMock . mockImplementation (() => {
throw new Error ( 'database malformed' )
})
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
2026-04-26 10:44:51 +08:00
const rows = await mod . searchSessionSummaries ( '123' , undefined , 10 )
expect ( rows ). toEqual ([])
2026-04-23 04:49:00 +02:00
expect ( likeAllMock ). not . toHaveBeenCalled ()
})
2026-04-17 12:56:41 -04:00
})