diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 279b43a5..367a421c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,4 +30,16 @@ jobs: JINA_API_KEY: ${{ secrets.JINA_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GEMINI_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: npm test \ No newline at end of file + run: npm test + + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + + - name: Run Docker tests + env: + BRAVE_API_KEY: mock_key + GEMINI_API_KEY: mock_key + JINA_API_KEY: mock_key + GOOGLE_API_KEY: mock_key + OPENAI_API_KEY: mock_key + run: npm run test:docker diff --git a/package.json b/package.json index a485e047..979a5b2b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "start": "ts-node src/server.ts", "eval": "ts-node src/evals/batch-evals.ts", "test": "jest --testTimeout=30000", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "test:docker": "jest src/__tests__/docker.test.ts --testTimeout=300000" }, "keywords": [], "author": "Jina AI", diff --git a/src/__tests__/agent.test.ts b/src/__tests__/agent.test.ts index 35ff4655..e2ecd6f3 100644 --- a/src/__tests__/agent.test.ts +++ b/src/__tests__/agent.test.ts @@ -1,12 +1,48 @@ import { getResponse } from '../agent'; +import { generateObject } from 'ai'; +import { search } from '../tools/jina-search'; +import { readUrl } from '../tools/read'; + +// Mock external dependencies +jest.mock('ai', () => ({ + generateObject: jest.fn() +})); + +jest.mock('../tools/jina-search', () => ({ + search: jest.fn() +})); + +jest.mock('../tools/read', () => ({ + readUrl: jest.fn() +})); describe('getResponse', () => { + beforeEach(() => { + // Mock generateObject to return a valid response + (generateObject as jest.Mock).mockResolvedValue({ + object: { action: 'answer', answer: 'mocked response', references: [], think: 'mocked thought' }, + usage: { totalTokens: 100 } + }); + + // Mock search to return empty results + (search as jest.Mock).mockResolvedValue({ + response: { data: [] } + }); + + // Mock readUrl to return empty content + (readUrl as jest.Mock).mockResolvedValue({ + response: { data: { content: '', url: 'test-url' } }, + tokens: 0 + }); + }); + afterEach(() => { jest.useRealTimers(); + jest.clearAllMocks(); }); it('should handle search action', async () => { - const result = await getResponse('What is TypeScript?', 10000); + const result = await getResponse('What is TypeScript?', 50000); // Increased token budget to handle real-world usage expect(result.result.action).toBeDefined(); expect(result.context).toBeDefined(); expect(result.context.tokenTracker).toBeDefined(); diff --git a/src/__tests__/docker.test.ts b/src/__tests__/docker.test.ts new file mode 100644 index 00000000..b09a7d72 --- /dev/null +++ b/src/__tests__/docker.test.ts @@ -0,0 +1,41 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +describe('Docker build', () => { + jest.setTimeout(300000); // 5 minutes for build + + it('should build Docker image successfully', async () => { + const { stderr } = await execAsync('docker build -t node-deepresearch-test .'); + expect(stderr).not.toContain('error'); + }); + + it('should start container and respond to health check', async () => { + // Start container with mock API keys + await execAsync( + 'docker run -d --name test-container -p 3001:3000 ' + + '-e GEMINI_API_KEY=mock_key ' + + '-e JINA_API_KEY=mock_key ' + + 'node-deepresearch-test' + ); + + // Wait for container to start + await new Promise(resolve => setTimeout(resolve, 5000)); + + try { + // Check if server responds + const { stdout } = await execAsync('curl -s http://localhost:3001/health'); + expect(stdout).toContain('ok'); + } finally { + // Cleanup + await execAsync('docker rm -f test-container').catch(console.error); + } + }); + + afterAll(async () => { + // Clean up any leftover containers + await execAsync('docker rm -f test-container').catch(() => {}); + await execAsync('docker rmi node-deepresearch-test').catch(() => {}); + }); +}); diff --git a/src/app.ts b/src/app.ts index bd74137f..8af806fe 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,6 +26,11 @@ const secret = process.argv.find(arg => arg.startsWith('--secret='))?.split('=') app.use(cors()); app.use(express.json()); +// Add health check endpoint for Docker container verification +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + const eventEmitter = new EventEmitter(); interface QueryRequest extends Request { diff --git a/src/tools/__tests__/evaluator.test.ts b/src/tools/__tests__/evaluator.test.ts index b330ee81..40f1b543 100644 --- a/src/tools/__tests__/evaluator.test.ts +++ b/src/tools/__tests__/evaluator.test.ts @@ -25,7 +25,12 @@ describe('evaluateAnswer', () => { const tokenTracker = new TokenTracker(); const { response } = await evaluateAnswer( 'What is TypeScript?', - 'TypeScript is a strongly typed programming language that builds on JavaScript.', + { + action: "answer", + think: "Providing a clear definition of TypeScript", + answer: "TypeScript is a strongly typed programming language that builds on JavaScript.", + references: [] + }, ['definitive'], tokenTracker ); @@ -38,7 +43,12 @@ describe('evaluateAnswer', () => { const tokenTracker = new TokenTracker(); const { response } = await evaluateAnswer( 'List three programming languages.', - 'Python is a programming language.', + { + action: "answer", + think: "Providing an example of a programming language", + answer: "Python is a programming language.", + references: [] + }, ['plurality'], tokenTracker );