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 ( [
{
id : 'node-wildcard-1' ,
source : 'cli' ,
user_id : '' ,
model : 'openai/gpt-5.4' ,
title : 'Node.js wildcard notes' ,
started_at : 1710004600 ,
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 : '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' ,
rank : 0 ,
} ,
] )
const mod = await import ( '../../packages/server/src/db/hermes/sessions-db' )
const rows = await mod . searchSessionSummaries ( 'naïve.js' , undefined , 10 )
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' )
} )
it ( 'escapes LIKE wildcards for literal special-character searches' , async ( ) = > {
titleAllMock . mockReturnValue ( [ ] )
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 ( [ ] )
2026-04-23 04:49:00 +02:00
likeAllMock . mockReturnValue ( [
{
2026-04-24 20:41:14 +08:00
id : 'cjk-literal-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 : 1710002980 ,
2026-04-23 04:49:00 +02:00
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 : '' ,
2026-04-24 20:41:14 +08:00
preview : '中文内容预览' ,
last_active : 1710002985 ,
2026-04-23 04:49:00 +02:00
matched_message_id : 10 ,
2026-04-24 20:41:14 +08:00
snippet : '这里也有记忆断裂' ,
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 ( )
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' )
2026-04-23 04:49:00 +02:00
} )
2026-04-24 20:41:14 +08:00
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
} )