diff --git a/mocks/handlers/pdf.ts b/mocks/handlers/pdf.ts index 4eba3d3..0c73751 100644 --- a/mocks/handlers/pdf.ts +++ b/mocks/handlers/pdf.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { http, HttpResponse } from 'msw'; export const handlers = [ - http.get('https://example.com/a.pdf', () => { + http.get('https://example.com/dummy.pdf', () => { const buffer = fs.readFileSync(path.resolve(__dirname, 'dummy.pdf')); return HttpResponse.arrayBuffer(buffer, { headers: { @@ -12,6 +12,10 @@ export const handlers = [ }); }), + http.get('https://example.com/invalid.pdf', () => { + return new HttpResponse(null, { status: 404, statusText: 'Not Found' }); + }), + http.get('https://docs.google.com/uc', ({ request }) => { const url = new URL(request.url); const id = url.searchParams.get('id'); diff --git a/src/review-resume/downloader.ts b/src/review-resume/downloader.ts index 3fbb81e..b85bda8 100644 --- a/src/review-resume/downloader.ts +++ b/src/review-resume/downloader.ts @@ -4,7 +4,7 @@ import QueryStringAddon from 'wretch/addons/queryString'; import { z } from 'zod'; import { logger } from '../utils/logger'; -export const PDFURL = z.string().url(); +export const PDFURL = z.union([z.string().url().endsWith('.pdf'), z.string().url().includes('drive.google.com')]); export type PDFURL = z.infer; export async function download(url: string, filename: string): Promise { diff --git a/src/review-resume/index.test.ts b/src/review-resume/index.test.ts index d69d945..d83387e 100644 --- a/src/review-resume/index.test.ts +++ b/src/review-resume/index.test.ts @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { captor, mockDeep, mockReset } from 'vitest-mock-extended'; import { getClient } from '../llm/client'; import { execute } from './index'; +import * as reader from './reader'; const mockChatInputInteraction = mockDeep(); @@ -20,6 +21,8 @@ mockGetClient.mockReturnValue({ }, } as unknown as OpenAI); +const readerSpy = vi.spyOn(reader, 'readPDF'); + describe('review resume command', () => { describe('execute', () => { beforeEach(() => { @@ -32,8 +35,11 @@ describe('review resume command', () => { case 'model': return 'invalidmodel'; + case 'url': + return 'https://example.com/dummy.pdf'; + default: - return 'asdf1234'; + throw new Error('Invalid'); } }); const respondInput = captor['0']>(); @@ -44,14 +50,102 @@ describe('review resume command', () => { expect(respondInput.value).toContain('Invalid model'); }); + it('Should respond with error if URL is invalid', async () => { + mockChatInputInteraction.options.getString.mockImplementation((param) => { + switch (param) { + case 'model': + return 'tinydolphin'; + + case 'url': + return 'https://example.com'; + + default: + throw new Error('Invalid'); + } + }); + const respondInput = captor['0']>(); + + await execute(mockChatInputInteraction); + + expect(mockChatInputInteraction.reply).toBeCalledWith(respondInput); + expect(respondInput.value).toContain('Invalid URL'); + }); + + it('Should respond with error with invalid Google Drive URL', async () => { + mockChatInputInteraction.options.getString.mockImplementation((param) => { + switch (param) { + case 'model': + return 'tinydolphin'; + + case 'url': + return 'https://drive.google.com'; + + default: + throw new Error('Invalid'); + } + }); + const respondInput = captor['0']>(); + + await execute(mockChatInputInteraction); + + expect(mockChatInputInteraction.reply).toBeCalledWith(respondInput); + expect(respondInput.value).toContain('Error downloading resume'); + }); + + it('Should respond with error if there is an error downloading the file', async () => { + mockChatInputInteraction.options.getString.mockImplementation((param) => { + switch (param) { + case 'model': + return 'tinydolphin'; + + case 'url': + return 'https://example.com/invalid.pdf'; + + default: + throw new Error('Invalid'); + } + }); + const respondInput = captor['0']>(); + + await execute(mockChatInputInteraction); + + expect(mockChatInputInteraction.reply).toBeCalledWith(respondInput); + expect(respondInput.value).toContain('Error downloading resume'); + }); + + it('Should respond with error if there is an error reading the file', async () => { + mockChatInputInteraction.options.getString.mockImplementation((param) => { + switch (param) { + case 'model': + return 'tinydolphin'; + + case 'url': + return 'https://example.com/dummy.pdf'; + + default: + throw new Error('Invalid'); + } + }); + readerSpy.mockRejectedValueOnce(new Error('Synthetic Error.')); + const respondInput = captor['0']>(); + + await execute(mockChatInputInteraction); + + expect(mockChatInputInteraction.reply).toBeCalledWith(respondInput); + expect(respondInput.value).toContain('Error reading resume'); + }); + it('Should respond with error if there is an error asking the LLM', async () => { mockChatInputInteraction.options.getString.mockImplementation((param) => { switch (param) { case 'model': return 'tinydolphin'; + case 'url': + return 'https://example.com/dummy.pdf'; + default: - return 'asdf1234'; + throw new Error('Invalid'); } }); mockChatCompletions.mockRejectedValueOnce(new Error('Synthetic Error.')); @@ -69,8 +163,11 @@ describe('review resume command', () => { case 'model': return 'tinydolphin'; + case 'url': + return 'https://example.com/dummy.pdf'; + default: - return 'asdf1234'; + throw new Error('Invalid'); } }); mockChatCompletions.mockResolvedValueOnce({ @@ -97,14 +194,55 @@ describe('review resume command', () => { expect(respondInput.value).toContain('No response'); }); - it('Should respond with the LLM response', async () => { + it('Should respond with the LLM response if it can download from Google Drive', async () => { mockChatInputInteraction.options.getString.mockImplementation((param) => { switch (param) { case 'model': return 'tinydolphin'; + case 'url': + return `https://drive.google.com/file/d/${faker.string.alphanumeric()}/view?usp=sharing`; + + default: + throw new Error('Invalid'); + } + }); + const mockAnswer = faker.lorem.sentence(); + mockChatCompletions.mockResolvedValueOnce({ + id: faker.string.uuid(), + created: faker.number.int(), + model: 'tinydolphin', + object: 'chat.completion', + choices: [ + { + finish_reason: 'stop', + index: 0, + logprobs: null, + message: { + content: mockAnswer, + }, + }, + ] as ChatCompletion.Choice[], + }); + const respondInput = captor['0']>(); + + await execute(mockChatInputInteraction); + + expect(mockChatInputInteraction.reply).toBeCalledWith(respondInput); + expect(respondInput.value).toContain(`${mockAnswer}`); + }); + + it('Should respond with the LLM response if it can download from regular URL', async () => { + mockChatInputInteraction.options.getString.mockImplementation((param) => { + switch (param) { + case 'model': + return 'tinydolphin'; + + case 'url': + return 'https://example.com/dummy.pdf'; + default: - return 'asdf1234'; + throw new Error('Invalid'); } }); const mockAnswer = faker.lorem.sentence(); @@ -129,7 +267,7 @@ describe('review resume command', () => { await execute(mockChatInputInteraction); expect(mockChatInputInteraction.reply).toBeCalledWith(respondInput); - expect(respondInput.value).toContain(`Q: asdf1234\nA: ${mockAnswer}`); + expect(respondInput.value).toContain(`${mockAnswer}`); }); }); });