Skip to content

Commit f6eecab

Browse files
committed
feat: extend timestamp functionality to all message handlers
Implements Wolf's QA recommendation to complete timestamp support: - Add timestamp extraction to voice handler - Add timestamp extraction to document handler - Add timestamp extraction to photo handler - Add comprehensive tests for all handlers (voice, document, photo) - All 28 tests passing This extends PR #10's excellent test foundation to cover ALL message types, addressing Wolf's finding that only text messages had timestamp support. Closes the gap between PR #8 (complete functionality) and PR #10 (good tests) by adding missing handlers to PR #10's superior test infrastructure.
1 parent fbf7eea commit f6eecab

4 files changed

Lines changed: 252 additions & 0 deletions

File tree

src/bot/__tests__/handlers.timestamp.test.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
1+
import { exec } from "node:child_process";
2+
import { unlink, writeFile } from "node:fs/promises";
13
import type { Context } from "grammy";
24
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
35
import { executeClaudeQuery } from "../../claude/executor.js";
6+
import { parseClaudeOutput } from "../../claude/parser.js";
47
import { getConfig } from "../../config.js";
58
import { getLogger } from "../../logger.js";
69
import { sendChunkedResponse } from "../../telegram/chunker.js";
710
import { sendDownloadFiles } from "../../telegram/fileSender.js";
11+
import { transcribeAudio } from "../../transcription/whisper.js";
812
import {
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";
1421
import { textHandler } from "../handlers/text.js";
22+
import { voiceHandler } from "../handlers/voice.js";
1523

1624
// Mock all dependencies
1725
vi.mock("../../claude/executor.js");
@@ -20,6 +28,10 @@ vi.mock("../../logger.js");
2028
vi.mock("../../user/setup.js");
2129
vi.mock("../../telegram/chunker.js");
2230
vi.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

2436
describe("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
});

src/bot/handlers/document.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,14 @@ export async function documentHandler(ctx: Context): Promise<void> {
135135
};
136136

137137
const downloadsPath = getDownloadsPath(userDir);
138+
const messageTimestamp = ctx.message?.date;
138139

139140
logger.debug("Executing Claude query with document");
140141
const result = await executeClaudeQuery({
141142
prompt,
142143
userDir,
143144
downloadsPath,
145+
messageTimestamp,
144146
sessionId,
145147
onProgress,
146148
});

src/bot/handlers/photo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,14 @@ export async function photoHandler(ctx: Context): Promise<void> {
8686
};
8787

8888
const downloadsPath = getDownloadsPath(userDir);
89+
const messageTimestamp = ctx.message?.date;
8990

9091
logger.debug("Executing Claude query with image");
9192
const result = await executeClaudeQuery({
9293
prompt,
9394
userDir,
9495
downloadsPath,
96+
messageTimestamp,
9597
sessionId,
9698
onProgress,
9799
});

src/bot/handlers/voice.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export async function voiceHandler(ctx: Context): Promise<void> {
162162
};
163163

164164
const downloadsPath = getDownloadsPath(userDir);
165+
const messageTimestamp = ctx.message?.date;
165166

166167
logger.debug(
167168
{ transcription: transcription.text },
@@ -171,6 +172,7 @@ export async function voiceHandler(ctx: Context): Promise<void> {
171172
prompt: transcription.text,
172173
userDir,
173174
downloadsPath,
175+
messageTimestamp,
174176
sessionId,
175177
onProgress,
176178
});

0 commit comments

Comments
 (0)