@@ -1404,5 +1404,112 @@ assert.ok(cumulativeResult.smartExtractionTriggered,
14041404assert . ok ( cumulativeResult . smartExtractionSkipped ,
14051405 "Turn 1 should skip smart extraction (cumulative=1 < 2). Logs: " +
14061406 cumulativeResult . logs . map ( ( e ) => e [ 1 ] ) . join ( " | " ) ) ;
1407+ // ============================================================
1408+ // Test: DM fallback — Fix-Must1b regression
1409+ // Scenario: DM conversation (no pending ingress texts).
1410+ // Smart extraction runs, LLM returns empty.
1411+ // Fix-Must1b: boundarySkipped=0 → early return → NO regex fallback.
1412+ // ============================================================
1413+
1414+ async function runDmFallbackMustfixScenario ( ) {
1415+ const workDir = mkdtempSync ( path . join ( tmpdir ( ) , "memory-dm-fallback-mustfix-" ) ) ;
1416+ const dbPath = path . join ( workDir , "db" ) ;
1417+ const logs = [ ] ;
1418+ let llmCalls = 0 ;
1419+ const embeddingServer = createEmbeddingServer ( ) ;
1420+
1421+ // LLM mock: ALWAYS returns empty memories.
1422+ // Simulates DM conversation where LLM finds no extractable content.
1423+ const llmServer = http . createServer ( async ( req , res ) => {
1424+ if ( req . method !== "POST" || req . url !== "/chat/completions" ) {
1425+ res . writeHead ( 404 ) ; res . end ( ) ; return ;
1426+ }
1427+ llmCalls += 1 ;
1428+ res . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
1429+ res . end ( JSON . stringify ( {
1430+ id : "chatcmpl-test" , object : "chat.completion" ,
1431+ created : Math . floor ( Date . now ( ) / 1000 ) , model : "mock-memory-model" ,
1432+ choices : [ { index : 0 , message : { role : "assistant" ,
1433+ content : JSON . stringify ( { memories : [ ] } ) } , finish_reason : "stop" } ] ,
1434+ } ) ) ;
1435+ } ) ;
1436+
1437+ await new Promise ( ( resolve ) => embeddingServer . listen ( 0 , "127.0.0.1" , resolve ) ) ;
1438+ await new Promise ( ( resolve ) => llmServer . listen ( 0 , "127.0.0.1" , resolve ) ) ;
1439+ const embeddingPort = embeddingServer . address ( ) . port ;
1440+ const llmPort = llmServer . address ( ) . port ;
1441+ process . env . TEST_EMBEDDING_BASE_URL = `http://127.0.0.1:${ embeddingPort } /v1` ;
1442+
1443+ try {
1444+ // extractMinMessages=1: first agent_end triggers smart extraction immediately.
1445+ // No message_received: pendingIngressTexts=[] (mimics DM with no conversationId).
1446+ const api = createMockApi (
1447+ dbPath , `http://127.0.0.1:${ embeddingPort } /v1` ,
1448+ `http://127.0.0.1:${ llmPort } ` , logs ,
1449+ { extractMinMessages : 1 , smartExtraction : true } ,
1450+ ) ;
1451+ plugin . register ( api ) ;
1452+ const sessionKey = "agent:main:discord:dm:user456" ;
1453+
1454+ await runAgentEndHook ( api , {
1455+ success : true ,
1456+ // No conversationId: simulates DM without pending ingress texts.
1457+ // sessionKey extracts to "discord:dm:user456" (truthy), but since
1458+ // message_received was never called, pendingIngressTexts Map has no entry.
1459+ messages : [ { role : "user" , content : "hi" } , { role : "user" , content : "hello?" } ] ,
1460+ } , { agentId : "main" , sessionKey } ) ;
1461+
1462+ const freshStore = new MemoryStore ( { dbPath, vectorDim : EMBEDDING_DIMENSIONS } ) ;
1463+ const entries = await freshStore . list ( [ "global" , "agent:main" ] , undefined , 10 , 0 ) ;
1464+ return { entries, llmCalls, logs } ;
1465+ } finally {
1466+ delete process . env . TEST_EMBEDDING_BASE_URL ;
1467+ await new Promise ( ( resolve ) => embeddingServer . close ( resolve ) ) ;
1468+ await new Promise ( ( resolve ) => llmServer . close ( resolve ) ) ;
1469+ rmSync ( workDir , { recursive : true , force : true } ) ;
1470+ }
1471+ }
1472+
1473+ const dmFallbackResult = await runDmFallbackMustfixScenario ( ) ;
1474+
1475+ // Assert 1: Smart extraction LLM was called exactly once
1476+ assert . equal ( dmFallbackResult . llmCalls , 1 ,
1477+ "Smart extraction should be called once. Logs: " +
1478+ dmFallbackResult . logs . map ( ( e ) => e [ 1 ] ) . join ( " | " ) ) ;
1479+
1480+ // Assert 2: No memories stored (regex fallback did NOT capture garbage)
1481+ assert . equal ( dmFallbackResult . entries . length , 0 ,
1482+ "No memories should be stored. Entries: " +
1483+ JSON . stringify ( dmFallbackResult . entries . map ( ( e ) => e . text ) ) ) ;
1484+
1485+ // Assert 3 (Fix-Must1b core): Early return triggered — skip regex fallback
1486+ assert . ok (
1487+ dmFallbackResult . logs . some ( ( entry ) =>
1488+ entry [ 1 ] . includes ( "skipping regex fallback" ) ) ,
1489+ "Fix-Must1b: should log 'skipping regex fallback'. Logs: " +
1490+ dmFallbackResult . logs . map ( ( e ) => e [ 1 ] ) . join ( " | " )
1491+ ) ;
1492+
1493+ // Assert 4: Regex fallback did NOT run
1494+ assert . ok (
1495+ dmFallbackResult . logs . every ( ( entry ) =>
1496+ ! entry [ 1 ] . includes ( "running regex fallback" ) ) ,
1497+ "Regex fallback should NOT run. Logs: " +
1498+ dmFallbackResult . logs . map ( ( e ) => e [ 1 ] ) . join ( " | " )
1499+ ) ;
1500+
1501+ // Assert 5: Smart extractor confirmed no memories extracted
1502+ assert . ok (
1503+ dmFallbackResult . logs . some ( ( entry ) =>
1504+ entry [ 1 ] . includes ( "no memories extracted" ) ) ,
1505+ "Smart extractor should report no memories extracted. Logs: " +
1506+ dmFallbackResult . logs . map ( ( e ) => e [ 1 ] ) . join ( " | " )
1507+ ) ;
1508+
1509+ // ============================================================
1510+ // End: Fix-Must1b regression test
1511+ // ============================================================
1512+
1513+
14071514
14081515console . log ( "OK: smart extractor branch regression test passed" ) ;
0 commit comments