1+ import { exec } from "node:child_process" ;
2+ import { unlink , writeFile } from "node:fs/promises" ;
13import type { Context } from "grammy" ;
24import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
35import { executeClaudeQuery } from "../../claude/executor.js" ;
6+ import { parseClaudeOutput } from "../../claude/parser.js" ;
47import { getConfig } from "../../config.js" ;
58import { getLogger } from "../../logger.js" ;
69import { sendChunkedResponse } from "../../telegram/chunker.js" ;
710import { sendDownloadFiles } from "../../telegram/fileSender.js" ;
11+ import { transcribeAudio } from "../../transcription/whisper.js" ;
812import {
913 ensureUserSetup ,
1014 getDownloadsPath ,
1115 getSessionId ,
16+ getUploadsPath ,
1217 saveSessionId ,
1318} from "../../user/setup.js" ;
19+ import { documentHandler } from "../handlers/document.js" ;
20+ import { photoHandler } from "../handlers/photo.js" ;
1421import { textHandler } from "../handlers/text.js" ;
22+ import { voiceHandler } from "../handlers/voice.js" ;
1523
1624// Mock all dependencies
1725vi . mock ( "../../claude/executor.js" ) ;
@@ -20,6 +28,10 @@ vi.mock("../../logger.js");
2028vi . mock ( "../../user/setup.js" ) ;
2129vi . mock ( "../../telegram/chunker.js" ) ;
2230vi . mock ( "../../telegram/fileSender.js" ) ;
31+ vi . mock ( "../../transcription/whisper.js" ) ;
32+ vi . mock ( "../../claude/parser.js" ) ;
33+ vi . mock ( "node:fs/promises" ) ;
34+ vi . mock ( "node:child_process" ) ;
2335
2436describe ( "Message Handlers - Timestamp Extraction" , ( ) => {
2537 beforeEach ( ( ) => {
@@ -28,6 +40,12 @@ describe("Message Handlers - Timestamp Extraction", () => {
2840 // Mock config
2941 vi . mocked ( getConfig ) . mockReturnValue ( {
3042 dataDir : "/test/data" ,
43+ telegram : {
44+ botToken : "test-bot-token" ,
45+ } ,
46+ transcription : {
47+ showTranscription : false ,
48+ } ,
3149 } as any ) ;
3250
3351 // Mock logger
@@ -40,6 +58,7 @@ describe("Message Handlers - Timestamp Extraction", () => {
4058 // Mock user setup functions
4159 vi . mocked ( ensureUserSetup ) . mockResolvedValue ( ) ;
4260 vi . mocked ( getDownloadsPath ) . mockReturnValue ( "/test/downloads" ) ;
61+ vi . mocked ( getUploadsPath ) . mockReturnValue ( "/test/uploads" ) ;
4362 vi . mocked ( getSessionId ) . mockResolvedValue ( "test-session" ) ;
4463 vi . mocked ( saveSessionId ) . mockResolvedValue ( ) ;
4564
@@ -53,6 +72,33 @@ describe("Message Handlers - Timestamp Extraction", () => {
5372 // Mock telegram functions
5473 vi . mocked ( sendChunkedResponse ) . mockResolvedValue ( ) ;
5574 vi . mocked ( sendDownloadFiles ) . mockResolvedValue ( 0 ) ;
75+
76+ // Mock additional functions for other handlers
77+ vi . mocked ( transcribeAudio ) . mockResolvedValue ( { text : "Transcribed text" } ) ;
78+ vi . mocked ( parseClaudeOutput ) . mockReturnValue ( {
79+ text : "Parsed response" ,
80+ sessionId : "parsed-session-id" ,
81+ } ) ;
82+
83+ // Mock file operations
84+ vi . mocked ( writeFile ) . mockResolvedValue ( ) ;
85+ vi . mocked ( unlink ) . mockResolvedValue ( ) ;
86+
87+ // Mock child_process (for ffmpeg in voice handler)
88+ vi . mocked ( exec ) . mockImplementation ( ( cmd , callback ) => {
89+ // Simulate successful ffmpeg execution
90+ setTimeout ( ( ) => {
91+ if ( typeof callback === "function" ) {
92+ callback ( null , "ffmpeg success" , "" ) ;
93+ }
94+ } , 0 ) ;
95+ return {
96+ pid : 123 ,
97+ stdout : { on : vi . fn ( ) } ,
98+ stderr : { on : vi . fn ( ) } ,
99+ on : vi . fn ( ) ,
100+ } as any ;
101+ } ) ;
56102 } ) ;
57103
58104 afterEach ( ( ) => {
@@ -321,4 +367,204 @@ describe("Message Handlers - Timestamp Extraction", () => {
321367 ) ;
322368 } ) ;
323369 } ) ;
370+
371+ describe ( "Voice Handler - Timestamp Extraction" , ( ) => {
372+ it ( "should extract timestamp from voice message and pass to executor" , async ( ) => {
373+ const testTimestamp = 1709520000 ; // March 4, 2024
374+
375+ const mockContext = {
376+ from : { id : 123 , username : "testuser" , first_name : "Test" } ,
377+ message : {
378+ voice : { file_id : "voice123" , duration : 10 , file_size : 5000 } ,
379+ date : testTimestamp ,
380+ } ,
381+ chat : { id : 456 } ,
382+ reply : vi . fn ( ) . mockResolvedValue ( { message_id : 789 } ) ,
383+ api : {
384+ getFile : vi . fn ( ) . mockResolvedValue ( { file_path : "voice/test.oga" } ) ,
385+ editMessageText : vi . fn ( ) . mockResolvedValue ( { } ) ,
386+ deleteMessage : vi . fn ( ) . mockResolvedValue ( { } ) ,
387+ } ,
388+ } as unknown as Context ;
389+
390+ // Mock global fetch
391+ global . fetch = vi . fn ( ) . mockResolvedValue ( {
392+ arrayBuffer : ( ) => Promise . resolve ( new ArrayBuffer ( 8 ) ) ,
393+ } as any ) ;
394+
395+ await voiceHandler ( mockContext ) ;
396+
397+ // Verify executeClaudeQuery was called with the timestamp
398+ expect ( executeClaudeQuery ) . toHaveBeenCalledWith (
399+ expect . objectContaining ( {
400+ messageTimestamp : testTimestamp ,
401+ } ) ,
402+ ) ;
403+ } ) ;
404+
405+ it ( "should handle undefined timestamp in voice message" , async ( ) => {
406+ const mockContext = {
407+ from : { id : 123 } ,
408+ message : {
409+ voice : { file_id : "voice123" , duration : 10 , file_size : 5000 } ,
410+ // date is undefined
411+ } ,
412+ chat : { id : 456 } ,
413+ reply : vi . fn ( ) . mockResolvedValue ( { message_id : 789 } ) ,
414+ api : {
415+ getFile : vi . fn ( ) . mockResolvedValue ( { file_path : "voice/test.oga" } ) ,
416+ editMessageText : vi . fn ( ) . mockResolvedValue ( { } ) ,
417+ deleteMessage : vi . fn ( ) . mockResolvedValue ( { } ) ,
418+ } ,
419+ } as unknown as Context ;
420+
421+ global . fetch = vi . fn ( ) . mockResolvedValue ( {
422+ arrayBuffer : ( ) => Promise . resolve ( new ArrayBuffer ( 8 ) ) ,
423+ } as any ) ;
424+
425+ await voiceHandler ( mockContext ) ;
426+
427+ expect ( executeClaudeQuery ) . toHaveBeenCalledWith (
428+ expect . objectContaining ( {
429+ messageTimestamp : undefined ,
430+ } ) ,
431+ ) ;
432+ } ) ;
433+ } ) ;
434+
435+ describe ( "Document Handler - Timestamp Extraction" , ( ) => {
436+ it ( "should extract timestamp from document message and pass to executor" , async ( ) => {
437+ const testTimestamp = 1709520000 ; // March 4, 2024
438+
439+ const mockContext = {
440+ from : { id : 123 } ,
441+ message : {
442+ document : {
443+ file_id : "doc123" ,
444+ file_name : "test.pdf" ,
445+ mime_type : "application/pdf" ,
446+ } ,
447+ caption : "Analyze this document" ,
448+ date : testTimestamp ,
449+ } ,
450+ chat : { id : 456 } ,
451+ reply : vi . fn ( ) . mockResolvedValue ( { message_id : 789 } ) ,
452+ api : {
453+ getFile : vi . fn ( ) . mockResolvedValue ( { file_path : "docs/test.pdf" } ) ,
454+ editMessageText : vi . fn ( ) . mockResolvedValue ( { } ) ,
455+ deleteMessage : vi . fn ( ) . mockResolvedValue ( { } ) ,
456+ } ,
457+ } as unknown as Context ;
458+
459+ global . fetch = vi . fn ( ) . mockResolvedValue ( {
460+ arrayBuffer : ( ) => Promise . resolve ( new ArrayBuffer ( 8 ) ) ,
461+ } as any ) ;
462+
463+ await documentHandler ( mockContext ) ;
464+
465+ expect ( executeClaudeQuery ) . toHaveBeenCalledWith (
466+ expect . objectContaining ( {
467+ messageTimestamp : testTimestamp ,
468+ } ) ,
469+ ) ;
470+ } ) ;
471+
472+ it ( "should handle undefined timestamp in document message" , async ( ) => {
473+ const mockContext = {
474+ from : { id : 123 } ,
475+ message : {
476+ document : {
477+ file_id : "doc123" ,
478+ file_name : "test.pdf" ,
479+ mime_type : "application/pdf" ,
480+ } ,
481+ caption : "Analyze this document" ,
482+ // date is undefined
483+ } ,
484+ chat : { id : 456 } ,
485+ reply : vi . fn ( ) . mockResolvedValue ( { message_id : 789 } ) ,
486+ api : {
487+ getFile : vi . fn ( ) . mockResolvedValue ( { file_path : "docs/test.pdf" } ) ,
488+ editMessageText : vi . fn ( ) . mockResolvedValue ( { } ) ,
489+ deleteMessage : vi . fn ( ) . mockResolvedValue ( { } ) ,
490+ } ,
491+ } as unknown as Context ;
492+
493+ global . fetch = vi . fn ( ) . mockResolvedValue ( {
494+ arrayBuffer : ( ) => Promise . resolve ( new ArrayBuffer ( 8 ) ) ,
495+ } as any ) ;
496+
497+ await documentHandler ( mockContext ) ;
498+
499+ expect ( executeClaudeQuery ) . toHaveBeenCalledWith (
500+ expect . objectContaining ( {
501+ messageTimestamp : undefined ,
502+ } ) ,
503+ ) ;
504+ } ) ;
505+ } ) ;
506+
507+ describe ( "Photo Handler - Timestamp Extraction" , ( ) => {
508+ it ( "should extract timestamp from photo message and pass to executor" , async ( ) => {
509+ const testTimestamp = 1709520000 ; // March 4, 2024
510+
511+ const mockContext = {
512+ from : { id : 123 } ,
513+ message : {
514+ photo : [ { file_id : "photo123" , width : 1920 , height : 1080 } ] ,
515+ caption : "What's in this image?" ,
516+ date : testTimestamp ,
517+ } ,
518+ chat : { id : 456 } ,
519+ reply : vi . fn ( ) . mockResolvedValue ( { message_id : 789 } ) ,
520+ api : {
521+ getFile : vi . fn ( ) . mockResolvedValue ( { file_path : "photos/test.jpg" } ) ,
522+ editMessageText : vi . fn ( ) . mockResolvedValue ( { } ) ,
523+ deleteMessage : vi . fn ( ) . mockResolvedValue ( { } ) ,
524+ } ,
525+ } as unknown as Context ;
526+
527+ global . fetch = vi . fn ( ) . mockResolvedValue ( {
528+ arrayBuffer : ( ) => Promise . resolve ( new ArrayBuffer ( 8 ) ) ,
529+ } as any ) ;
530+
531+ await photoHandler ( mockContext ) ;
532+
533+ expect ( executeClaudeQuery ) . toHaveBeenCalledWith (
534+ expect . objectContaining ( {
535+ messageTimestamp : testTimestamp ,
536+ } ) ,
537+ ) ;
538+ } ) ;
539+
540+ it ( "should handle undefined timestamp in photo message" , async ( ) => {
541+ const mockContext = {
542+ from : { id : 123 } ,
543+ message : {
544+ photo : [ { file_id : "photo123" , width : 1920 , height : 1080 } ] ,
545+ caption : "What's in this image?" ,
546+ // date is undefined
547+ } ,
548+ chat : { id : 456 } ,
549+ reply : vi . fn ( ) . mockResolvedValue ( { message_id : 789 } ) ,
550+ api : {
551+ getFile : vi . fn ( ) . mockResolvedValue ( { file_path : "photos/test.jpg" } ) ,
552+ editMessageText : vi . fn ( ) . mockResolvedValue ( { } ) ,
553+ deleteMessage : vi . fn ( ) . mockResolvedValue ( { } ) ,
554+ } ,
555+ } as unknown as Context ;
556+
557+ global . fetch = vi . fn ( ) . mockResolvedValue ( {
558+ arrayBuffer : ( ) => Promise . resolve ( new ArrayBuffer ( 8 ) ) ,
559+ } as any ) ;
560+
561+ await photoHandler ( mockContext ) ;
562+
563+ expect ( executeClaudeQuery ) . toHaveBeenCalledWith (
564+ expect . objectContaining ( {
565+ messageTimestamp : undefined ,
566+ } ) ,
567+ ) ;
568+ } ) ;
569+ } ) ;
324570} ) ;
0 commit comments