Skip to content

Commit 5d7010d

Browse files
committed
test files to validate functionalities for services including source linking, advanced search, Markdown enhancement, PDF processing, S3 image viewing, and article generation.
Took 42 seconds
1 parent ff9d5e9 commit 5d7010d

21 files changed

+2153
-40
lines changed

tests/article-generator.test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import fs from 'fs/promises'
5+
import path from 'path'
6+
import { ArticleGenerator } from '@/lib/services/article-generator'
7+
import dotenv from 'dotenv'
8+
9+
// Load environment variables
10+
dotenv.config({ path: '.env.local' })
11+
describe("Article Generator", () => {
12+
jest.setTimeout(120000) // 2 minute timeout for LLM processing
13+
14+
const saveArticle = async (filename: string, content: string) => {
15+
const testOutputDir = path.join(process.cwd(), 'test-output')
16+
await fs.mkdir(testOutputDir, { recursive: true })
17+
const absolutePath = path.join(testOutputDir, filename)
18+
await fs.writeFile(absolutePath, content, 'utf-8')
19+
console.log('\x1b[36m%s\x1b[0m', `📝 Article saved to: ${absolutePath}`)
20+
return absolutePath
21+
}
22+
23+
const generateMarkdown = (article: any) => {
24+
let markdown = `# ${article.title}\n\n`
25+
26+
// Add metadata
27+
markdown += `> Generated: ${new Date().toISOString()}\n`
28+
markdown += `> Author: ${article.metadata.author}\n`
29+
markdown += `> Category: ${article.metadata.categories.join(', ')}\n`
30+
markdown += `> Tags: ${article.metadata.tags.join(', ')}\n\n`
31+
32+
// Add featured image if available
33+
if (article.featured_image) {
34+
markdown += `![${article.featured_image_alt}](${article.featured_image})\n\n`
35+
}
36+
37+
// Add content with images
38+
markdown += article.content
39+
40+
// Add SEO metadata
41+
markdown += '\n\n---\n\n'
42+
markdown += '## SEO Metadata\n\n'
43+
markdown += `- Title: ${article.metadata.seo_title}\n`
44+
markdown += `- Description: ${article.metadata.seo_description}\n`
45+
markdown += `- Keywords: ${article.metadata.seo_keywords.join(', ')}\n`
46+
47+
return markdown
48+
}
49+
50+
it("generates an article with proper source attribution", async () => {
51+
// Skip if required environment variables are not set
52+
if (!process.env.TAVILY_API_KEY) {
53+
throw new Error('TAVILY_API_KEY is not set')
54+
}
55+
if (!process.env.OPENAI_API_KEY) {
56+
throw new Error('OPENAI_API_KEY is not set')
57+
}
58+
59+
const outputDir = path.join(process.cwd(), 'test-output')
60+
const articleGenerator = new ArticleGenerator(
61+
process.env.TAVILY_API_KEY,
62+
outputDir
63+
)
64+
65+
const topic = "Latest developments in quantum computing"
66+
await articleGenerator.generateArticle(topic)
67+
68+
// Check if the article and raw data files were created
69+
const articlePath = path.join(
70+
outputDir,
71+
'latest-developments-in-quantum-computing.md'
72+
)
73+
const rawDataPath = path.join(
74+
outputDir,
75+
'latest-developments-in-quantum-computing.json'
76+
)
77+
78+
// Read both files
79+
const articleContent = await fs.readFile(articlePath, 'utf-8')
80+
const rawData = JSON.parse(await fs.readFile(rawDataPath, 'utf-8'))
81+
82+
// Check for markdown links
83+
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
84+
const links = Array.from(articleContent.matchAll(markdownLinkRegex))
85+
86+
// Verify we have links
87+
expect(links.length).toBeGreaterThan(0)
88+
89+
// Get all URLs from the raw search results
90+
const sourceUrls = rawData.results.map((result: { url: string }) => result.url)
91+
92+
// Get all URLs from the markdown links
93+
const linkedUrls = links.map(match => match[2])
94+
95+
// Verify that at least some of our source URLs are used in the article
96+
const usedSourceUrls = sourceUrls.filter((url: string) => linkedUrls.includes(url))
97+
expect(usedSourceUrls.length).toBeGreaterThan(0)
98+
99+
// Verify link text is natural (not just "source" or "here")
100+
links.forEach(([_, linkText]) => {
101+
expect(linkText.toLowerCase()).not.toBe('source')
102+
expect(linkText.toLowerCase()).not.toBe('here')
103+
expect(linkText.length).toBeGreaterThan(3)
104+
})
105+
106+
// Log link statistics
107+
console.log('\x1b[36m%s\x1b[0m', `📝 Link Stats:`)
108+
console.log('\x1b[36m%s\x1b[0m', `- Total links: ${links.length}`)
109+
console.log('\x1b[36m%s\x1b[0m', `- Unique sources linked: ${new Set(linkedUrls).size}`)
110+
console.log('\x1b[36m%s\x1b[0m', `- Source URLs used: ${usedSourceUrls.length}/${sourceUrls.length}`)
111+
112+
// Log the file location
113+
console.log('\x1b[36m%s\x1b[0m', `📝 Article saved to: ${articlePath}`)
114+
console.log('\x1b[36m%s\x1b[0m', `📝 Raw data saved to: ${rawDataPath}`)
115+
})
116+
117+
it("generates an article about CRISPR with proper formatting", async () => {
118+
// Skip if required environment variables are not set
119+
if (!process.env.TAVILY_API_KEY) {
120+
throw new Error('TAVILY_API_KEY is not set')
121+
}
122+
if (!process.env.OPENAI_API_KEY) {
123+
throw new Error('OPENAI_API_KEY is not set')
124+
}
125+
126+
const outputDir = path.join(process.cwd(), 'test-output')
127+
const articleGenerator = new ArticleGenerator(
128+
process.env.TAVILY_API_KEY,
129+
outputDir
130+
)
131+
132+
const topic = "CRISPR gene editing technology and its medical applications"
133+
await articleGenerator.generateArticle(topic)
134+
135+
// Check if the article was created
136+
const articlePath = path.join(
137+
outputDir,
138+
'crispr-gene-editing-technology-and-its-medical-applications.md'
139+
)
140+
const fileExists = await fs.access(articlePath)
141+
.then(() => true)
142+
.catch(() => false)
143+
144+
expect(fileExists).toBe(true)
145+
146+
// Read and validate the article
147+
const articleContent = await fs.readFile(articlePath, 'utf-8')
148+
149+
// Content validation
150+
expect(articleContent).toContain('CRISPR')
151+
expect(articleContent).toMatch(/!\[.*?\]\(.*?\)/) // Should contain images
152+
expect(articleContent).toMatch(/\[.*?\]\(.*?\)/) // Should contain links
153+
expect(articleContent).toContain('## SEO Metadata') // Should have SEO section
154+
155+
// Log the file location
156+
console.log('\x1b[36m%s\x1b[0m', `📝 Article saved to: ${articlePath}`)
157+
})
158+
})

tests/fdai.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe("FDAi Tests", () => {
2020
const article = await writeArticle("The most effective experimental treatments for long covid",
2121
"test-user",
2222
{
23-
modelName: "claude-3-5-sonnet-20240620",
23+
//modelName: "claude-3-5-sonnet-20240620",
2424
})
2525
console.log(dumpTypeDefinition(article))
2626
expect(article).not.toBeNull()
@@ -56,7 +56,7 @@ describe("FDAi Tests", () => {
5656
const result = await doMetaAnalysis("MDMA-Assisted Psychotherapy", "PTSD");
5757
console.log(result)
5858
writeFileSync("meta-analysis.json", JSON.stringify(result, null, 2))
59-
})
59+
}, 60000)
6060
it("gets dfda access token", async () => {
6161
const testUser = await getOrCreateTestUser()
6262
const result = await getOrCreateDfdaAccessToken(testUser.id)

tests/finance-agent.test.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
import { estimateETFGain, analyzeMultipleIndustries } from "@/lib/finance/finance-agent"
5+
import { ModelName } from "@/lib/utils/modelUtils"
6+
7+
describe("Finance Agent tests", () => {
8+
jest.setTimeout(60000) // 1-minute timeout
9+
10+
it("estimates ETF gain for Biotechnology industry with different models", async () => {
11+
const industryName = "Biotechnology"
12+
const models: ModelName[] = [
13+
'gemini-1.5-pro',
14+
'claude-3-5-sonnet-20240620'
15+
]
16+
17+
for (const modelName of models) {
18+
console.log(`\nTesting with model: ${modelName}`)
19+
const result = await estimateETFGain(industryName, modelName)
20+
21+
// Check the structure of the response
22+
expect(result).toHaveProperty("industryName")
23+
expect(result).toHaveProperty("parameters")
24+
expect(result).toHaveProperty("results")
25+
expect(result).toHaveProperty("tokenUsage")
26+
27+
// Check parameters
28+
expect(result.parameters).toHaveProperty("regulatoryCost")
29+
expect(result.parameters).toHaveProperty("percentageOfRevenue")
30+
expect(result.parameters).toHaveProperty("potentialDecrease")
31+
expect(result.parameters).toHaveProperty("netProfitMargin")
32+
33+
// Validate parameter ranges
34+
expect(result.parameters.regulatoryCost).toBeGreaterThan(0)
35+
expect(result.parameters.percentageOfRevenue).toBeGreaterThan(0)
36+
expect(result.parameters.percentageOfRevenue).toBeLessThan(100)
37+
expect(result.parameters.potentialDecrease).toBeGreaterThan(0)
38+
expect(result.parameters.potentialDecrease).toBeLessThan(100)
39+
expect(result.parameters.netProfitMargin).toBeGreaterThan(0)
40+
expect(result.parameters.netProfitMargin).toBeLessThan(100)
41+
42+
// Check results
43+
expect(result.results).toHaveProperty("industryRevenue")
44+
expect(result.results).toHaveProperty("currentNetIncome")
45+
expect(result.results).toHaveProperty("deltaC")
46+
expect(result.results).toHaveProperty("percentIncreaseNetIncome")
47+
expect(result.results).toHaveProperty("estimatedETFIncrease")
48+
49+
// Validate calculations
50+
expect(result.results.industryRevenue).toBeGreaterThan(0)
51+
expect(result.results.currentNetIncome).toBeGreaterThan(0)
52+
expect(result.results.deltaC).toBeGreaterThan(0)
53+
expect(result.results.percentIncreaseNetIncome).toBeGreaterThan(0)
54+
expect(result.results.estimatedETFIncrease).toBeGreaterThan(0)
55+
56+
// Optional: Log the result for manual inspection
57+
console.log(`${modelName} Analysis:`, JSON.stringify(result, null, 2))
58+
}
59+
})
60+
61+
it("estimates ETF gain for Financial Services industry", async () => {
62+
const industryName = "Financial Services"
63+
const result = await estimateETFGain(industryName)
64+
65+
// Basic structure checks
66+
expect(result).toBeDefined()
67+
expect(result.industryName).toBe(industryName)
68+
69+
// Check token usage
70+
expect(result.tokenUsage).toBeDefined()
71+
expect(result.tokenUsage?.totalTokens).toBeGreaterThan(0)
72+
73+
// Validate that calculations are consistent
74+
const { parameters, results } = result
75+
76+
// Calculate industry revenue manually to verify
77+
const calculatedRevenue = parameters.regulatoryCost / (parameters.percentageOfRevenue / 100)
78+
expect(results.industryRevenue).toBeCloseTo(calculatedRevenue, 5)
79+
80+
// Calculate net income manually to verify
81+
const calculatedNetIncome = results.industryRevenue * (parameters.netProfitMargin / 100)
82+
expect(results.currentNetIncome).toBeCloseTo(calculatedNetIncome, 5)
83+
84+
// Calculate regulatory cost savings manually to verify
85+
const calculatedSavings = parameters.regulatoryCost * (parameters.potentialDecrease / 100)
86+
expect(results.deltaC).toBeCloseTo(calculatedSavings, 5)
87+
88+
console.log("Financial Services Analysis:", JSON.stringify(result, null, 2))
89+
})
90+
91+
it("provides consistent results for the same industry", async () => {
92+
const industryName = "Healthcare"
93+
94+
// Make two separate calls
95+
const result1 = await estimateETFGain(industryName)
96+
const result2 = await estimateETFGain(industryName)
97+
98+
// Results should be similar but not identical due to AI variation
99+
// We'll check if they're within reasonable ranges of each other
100+
const marginOfError = 0.5 // 50% margin to account for AI variation
101+
102+
expect(
103+
Math.abs(result1.parameters.regulatoryCost - result2.parameters.regulatoryCost) /
104+
result1.parameters.regulatoryCost
105+
).toBeLessThan(marginOfError)
106+
107+
expect(
108+
Math.abs(result1.parameters.percentageOfRevenue - result2.parameters.percentageOfRevenue) /
109+
result1.parameters.percentageOfRevenue
110+
).toBeLessThan(marginOfError)
111+
112+
console.log("Consistency Test Results:", {
113+
firstCall: result1,
114+
secondCall: result2
115+
})
116+
})
117+
118+
it("analyzes multiple industries with different models", async () => {
119+
const industries = [
120+
"Biotechnology",
121+
"Financial Services",
122+
"Healthcare"
123+
];
124+
125+
const models: ModelName[] = [
126+
'gemini-1.5-pro',
127+
'claude-3-5-sonnet-20240620'
128+
]
129+
130+
for (const modelName of models) {
131+
console.log(`\n=== Testing with model: ${modelName} ===`)
132+
133+
const results = await analyzeMultipleIndustries(industries, modelName);
134+
135+
// Check we got results for all industries
136+
expect(results.length).toBe(industries.length);
137+
138+
// Check results are sorted by ETF increase
139+
for (let i = 0; i < results.length - 1; i++) {
140+
expect(results[i].results.estimatedETFIncrease)
141+
.toBeGreaterThanOrEqual(results[i + 1].results.estimatedETFIncrease);
142+
}
143+
144+
// Verify structure of each result
145+
results.forEach(result => {
146+
// Check basic structure
147+
expect(result).toHaveProperty("industryName");
148+
expect(result).toHaveProperty("parameters");
149+
expect(result).toHaveProperty("results");
150+
151+
// Check parameters are within reasonable ranges
152+
expect(result.parameters.regulatoryCost).toBeGreaterThan(0);
153+
expect(result.parameters.percentageOfRevenue).toBeLessThan(100);
154+
expect(result.parameters.potentialDecrease).toBeLessThan(100);
155+
expect(result.parameters.netProfitMargin).toBeLessThan(100);
156+
157+
// Verify calculations for each industry
158+
const calculatedRevenue = result.parameters.regulatoryCost /
159+
(result.parameters.percentageOfRevenue / 100);
160+
expect(result.results.industryRevenue).toBeCloseTo(calculatedRevenue, 5);
161+
});
162+
163+
// Check that industries are unique
164+
const uniqueIndustries = new Set(results.map(r => r.industryName));
165+
expect(uniqueIndustries.size).toBe(industries.length);
166+
167+
console.log(`\n${modelName} Multi-Industry Analysis:`,
168+
JSON.stringify(results, null, 2));
169+
}
170+
}, 480000); // 8-minute timeout for multiple models and industries
171+
172+
it("compares results between different models", async () => {
173+
const industryName = "Healthcare";
174+
const models: ModelName[] = [
175+
'gemini-1.5-pro',
176+
'claude-3-5-sonnet-20240620'
177+
];
178+
179+
const results = await Promise.all(
180+
models.map(model => estimateETFGain(industryName, model))
181+
);
182+
183+
// Compare results between models
184+
results.forEach((result, i) => {
185+
console.log(`\n${models[i]} results:`)
186+
console.log(`ETF Increase: ${result.results.estimatedETFIncrease.toFixed(2)}%`)
187+
console.log(`Regulatory Cost: $${result.parameters.regulatoryCost.toFixed(2)}B`)
188+
console.log(`Token Usage: ${result.tokenUsage?.totalTokens || 'N/A'}`)
189+
});
190+
191+
// Check that results are within reasonable ranges of each other
192+
const marginOfError = 0.5; // 50% margin
193+
for (let i = 0; i < results.length - 1; i++) {
194+
const ratio = Math.abs(
195+
results[i].results.estimatedETFIncrease -
196+
results[i + 1].results.estimatedETFIncrease
197+
) / results[i].results.estimatedETFIncrease;
198+
199+
expect(ratio).toBeLessThan(marginOfError);
200+
}
201+
}, 120000); // 2-minute timeout for model comparison
202+
})

0 commit comments

Comments
 (0)