Skip to content

Commit 65e6a87

Browse files
committed
test: add base e2e test for the RAG example
1 parent 4d09f41 commit 65e6a87

File tree

2 files changed

+299
-70
lines changed

2 files changed

+299
-70
lines changed

test/e2e-nokey/rag.e2e.test.js

Lines changed: 0 additions & 70 deletions
This file was deleted.

test/e2e/rag.e2e.test.js

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
process.env.API_TEST_FILE = 'e2e/rag.e2e.test.js';
2+
const { test, expect } = require('@playwright/test');
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { MongoClient } = require('mongodb');
6+
const { registerTestInManifest, isInManifest } = require('../tools/fixture-helpers');
7+
8+
// Self-register this test in the manifest when recording
9+
registerTestInManifest('e2e/rag.e2e.test.js');
10+
11+
// Skip this file during replay if it's not in the manifest
12+
if (process.env.API_MODE === 'replay' && !isInManifest('e2e/rag.e2e.test.js')) {
13+
console.log('[fixtures] skipping e2e/rag.e2e.test.js as it is not in manifest for replay mode - 2 tests');
14+
test.skip(true, 'Not in manifest for replay mode');
15+
}
16+
17+
/**
18+
* Create a minimal PDF file with ExampleCorp test data
19+
*/
20+
function writeMinimalPdf(filePath) {
21+
const text = `
22+
ExampleCorp was founded in 2019.
23+
Its headquarters are located in Seattle, Washington.
24+
25+
The company reported revenue of $12 million in 2023.
26+
Net income for 2023 was $1.2 million.
27+
28+
ExampleCorp operates in the cloud services market.
29+
Its primary competitors include AlphaCloud and NimbusCo.
30+
31+
In 2022, revenue was reported as $9 million.
32+
The company does not operate in Europe.
33+
34+
This document contains no information about executive compensation.
35+
Any claim about CEO salary is unsupported.
36+
`.trim();
37+
38+
const escaped = text.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
39+
40+
const pdf = `%PDF-1.4
41+
1 0 obj
42+
<< /Type /Catalog /Pages 2 0 R >>
43+
endobj
44+
2 0 obj
45+
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
46+
endobj
47+
3 0 obj
48+
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
49+
/Contents 4 0 R
50+
/Resources << /Font << /F1 5 0 R >> >>
51+
>>
52+
endobj
53+
4 0 obj
54+
<< /Length ${escaped.length + 73} >>
55+
stream
56+
BT
57+
/F1 12 Tf
58+
72 720 Td
59+
(${escaped.replace(/\n/g, ') Tj\n0 -14 Td\n(')}) Tj
60+
ET
61+
endstream
62+
endobj
63+
5 0 obj
64+
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
65+
endobj
66+
xref
67+
0 6
68+
0000000000 65535 f
69+
0000000010 00000 n
70+
0000000060 00000 n
71+
0000000117 00000 n
72+
0000000275 00000 n
73+
0000000450 00000 n
74+
trailer
75+
<< /Size 6 /Root 1 0 R >>
76+
startxref
77+
520
78+
%%EOF`;
79+
80+
fs.writeFileSync(filePath, pdf);
81+
}
82+
83+
test.describe('RAG File Upload Integration', () => {
84+
test.describe.configure({ mode: 'serial' });
85+
// Helper to remove 'test-*' files from RAG input and 'ingested' dirs
86+
const cleanupTestFiles = () => {
87+
const ragInputDir = path.join(__dirname, '../../rag_input');
88+
const ingestedDir = path.join(ragInputDir, 'ingested');
89+
90+
// Remove any test artifacts in both directories
91+
[ragInputDir, ingestedDir].forEach((dir) => {
92+
if (fs.existsSync(dir)) {
93+
const files = fs.readdirSync(dir).filter((f) => f.startsWith('test-'));
94+
files.forEach((file) => {
95+
const filePath = path.join(dir, file);
96+
if (fs.existsSync(filePath)) {
97+
fs.unlinkSync(filePath);
98+
}
99+
});
100+
}
101+
});
102+
};
103+
104+
test.beforeEach(async () => {
105+
// Ensure a clean slate before each test run
106+
cleanupTestFiles();
107+
});
108+
109+
test.afterEach(async () => {
110+
// Remove test artifacts after each test to keep state isolated
111+
cleanupTestFiles();
112+
});
113+
114+
test.afterAll(async () => {
115+
// Clean up MongoDB rag_chunks collection after all tests
116+
// Remove all documents that have fileName: 'examplecorp_test_fixture.pdf'
117+
const client = new MongoClient(process.env.MONGODB_URI);
118+
try {
119+
await client.connect();
120+
const db = client.db();
121+
const collection = db.collection('rag_chunks');
122+
const result = await collection.deleteMany({ fileName: 'examplecorp_test_fixture.pdf' });
123+
console.log(`Cleaned up ${result.deletedCount} documents from rag_chunks collection`);
124+
} catch (err) {
125+
console.error('Error cleaning up rag_chunks:', err);
126+
} finally {
127+
await client.close();
128+
}
129+
});
130+
131+
test('should validate question submission functionality', async ({ page }) => {
132+
// Navigate to RAG page
133+
await page.goto('/ai/rag');
134+
await page.waitForLoadState('networkidle');
135+
136+
// Set empty value and remove 'required' to exercise server-side validation
137+
await page.fill('#question', '');
138+
139+
// Remove the required attribute to bypass client-side validation
140+
await page.evaluate(() => {
141+
const questionField = document.getElementById('question');
142+
if (questionField) {
143+
questionField.removeAttribute('required');
144+
}
145+
});
146+
147+
// Try to submit empty question by clicking the ask button
148+
await page.click('#ask-btn');
149+
150+
// Wait for redirect to complete and for flash messages to render
151+
await page.waitForLoadState('networkidle');
152+
153+
const errorAlert = page.locator('.alert-danger');
154+
155+
await expect(errorAlert).toBeVisible({ timeout: 3000 });
156+
157+
// Locate server-side validation error alert
158+
const hasError = (await errorAlert.count()) > 0;
159+
160+
// Ensure error alert appears with expected validation message
161+
expect(hasError).toBeTruthy();
162+
await expect(errorAlert).toBeVisible();
163+
await expect(errorAlert).toContainText(/Please enter a question./i);
164+
});
165+
166+
test('should ingest ExampleCorp PDF and answer revenue question', async ({ page }) => {
167+
// Increase timeout for this test due to 30 second wait for ingestion
168+
test.setTimeout(120000);
169+
170+
// Create test PDF dynamically
171+
const ragInputDir = path.join(__dirname, '../../rag_input');
172+
const targetFile = path.join(ragInputDir, 'examplecorp_test_fixture.pdf');
173+
const ingestedFile = path.join(ragInputDir, 'ingested', 'examplecorp_test_fixture.pdf');
174+
175+
// Ensure rag_input directory exists
176+
if (!fs.existsSync(ragInputDir)) {
177+
fs.mkdirSync(ragInputDir, { recursive: true });
178+
}
179+
180+
// Create the PDF file with test data
181+
writeMinimalPdf(targetFile);
182+
183+
try {
184+
// Navigate to RAG page
185+
await page.goto('/ai/rag');
186+
await page.waitForLoadState('networkidle');
187+
188+
// Click the "Ingest Files" button
189+
const ingestBtn = page.locator('#ingest-btn');
190+
await expect(ingestBtn).toBeVisible();
191+
await ingestBtn.click();
192+
193+
// Wait for ingestion to complete (redirect back to page)
194+
await page.waitForLoadState('networkidle');
195+
196+
// Verify ingestion was successful (info messages use .alert-primary, not .alert-info)
197+
const successAlert = page.locator('.alert-success, .alert-primary');
198+
const errorAlert = page.locator('.alert-danger');
199+
200+
await expect(successAlert.or(errorAlert)).toBeVisible({ timeout: 5000 });
201+
202+
// If there's an error, fail with the error message
203+
if ((await errorAlert.count()) > 0) {
204+
const errorText = await errorAlert.textContent();
205+
throw new Error(`Ingestion failed: ${errorText}`);
206+
}
207+
208+
await expect(successAlert).toBeVisible();
209+
210+
// Verify the file appears in the Ingested Files list
211+
const fileInList = page.locator('table.table-striped tbody tr td', { hasText: 'examplecorp_test_fixture.pdf' });
212+
await expect(fileInList).toBeVisible({ timeout: 5000 });
213+
214+
// Poll for index readiness instead of blind wait
215+
// MongoDB Atlas Search indexes can take time to build after ingestion
216+
// We poll by attempting to ask a question and checking for "index is not ready" error
217+
let indexReady = false;
218+
const maxAttempts = 12; // 12 attempts * 5 seconds = 60 seconds max wait
219+
220+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
221+
console.log(`Checking index readiness (attempt ${attempt}/${maxAttempts})...`);
222+
223+
// Fill in a test question
224+
await page.fill('#question', 'How much money ExampleCorp made in 2023');
225+
226+
// Click the ask button
227+
await page.click('#ask-btn');
228+
229+
// Wait for response to load
230+
await page.waitForLoadState('networkidle');
231+
232+
// Check if we got an "index is not ready" error
233+
const errorAlert = page.locator('.alert-danger');
234+
if ((await errorAlert.count()) > 0) {
235+
const errorText = await errorAlert.textContent();
236+
if (errorText.includes('index is not ready') || errorText.includes('not ready')) {
237+
console.log(`Index not ready yet: ${errorText.substring(0, 100)}...`);
238+
if (attempt < maxAttempts) {
239+
// Wait 5 seconds before next attempt
240+
await page.waitForTimeout(5000);
241+
// Navigate back to RAG page to try again
242+
await page.goto('/ai/rag');
243+
await page.waitForLoadState('networkidle');
244+
continue;
245+
}
246+
} else {
247+
// Different error - fail immediately
248+
throw new Error(`Unexpected error: ${errorText}`);
249+
}
250+
} else {
251+
// No error - index is ready and we got a response
252+
console.log('Index is ready!');
253+
indexReady = true;
254+
break;
255+
}
256+
}
257+
258+
if (!indexReady) {
259+
throw new Error('Index did not become ready within the timeout period');
260+
}
261+
262+
// At this point, we have a response on the page from the polling loop above
263+
// Verify we got a valid response
264+
const ragResponseBox = page.locator('.response-box').first();
265+
const hasResponse = (await ragResponseBox.count()) > 0;
266+
expect(hasResponse).toBeTruthy();
267+
268+
// Verify the RAG response contains the expected values ($12 and $1.2)
269+
const ragResponsePre = page.locator('.response-box pre').first();
270+
await expect(ragResponsePre).toBeVisible();
271+
const ragResponseText = await ragResponsePre.textContent();
272+
expect(ragResponseText).toContain('$12');
273+
expect(ragResponseText).toContain('$1.2');
274+
275+
// Verify the No-RAG LLM Response is present (the system shows both responses)
276+
const noRagResponseBoxes = page.locator('.response-box');
277+
expect(await noRagResponseBoxes.count()).toBeGreaterThanOrEqual(2);
278+
279+
// The second response box is the No-RAG response
280+
// It should NOT contain the specific dollar amounts from the PDF since it doesn't have RAG context
281+
const noRagResponsePre = noRagResponseBoxes.nth(1).locator('pre');
282+
await expect(noRagResponsePre).toBeVisible();
283+
const noRagResponseText = await noRagResponsePre.textContent();
284+
285+
// The No-RAG response should not have the specific ExampleCorp data
286+
expect(noRagResponseText).not.toContain('$12');
287+
expect(noRagResponseText).not.toContain('$1.2');
288+
} finally {
289+
// Clean up: remove the test file from rag_input if still there
290+
if (fs.existsSync(targetFile)) {
291+
fs.unlinkSync(targetFile);
292+
}
293+
// Clean up: remove the file from ingested directory
294+
if (fs.existsSync(ingestedFile)) {
295+
fs.unlinkSync(ingestedFile);
296+
}
297+
}
298+
});
299+
});

0 commit comments

Comments
 (0)