|
|
|
@ -120,6 +120,7 @@ test.describe('Live generation with OpenAI', () => {
|
|
|
|
|
|
|
|
|
|
|
|
test('full generation pipeline produces valid synthesis', async ({
|
|
|
|
test('full generation pipeline produces valid synthesis', async ({
|
|
|
|
page,
|
|
|
|
page,
|
|
|
|
|
|
|
|
request,
|
|
|
|
}) => {
|
|
|
|
}) => {
|
|
|
|
test.setTimeout(180_000);
|
|
|
|
test.setTimeout(180_000);
|
|
|
|
|
|
|
|
|
|
|
|
@ -141,6 +142,8 @@ test.describe('Live generation with OpenAI', () => {
|
|
|
|
ai_provider: 'openai',
|
|
|
|
ai_provider: 'openai',
|
|
|
|
ai_model: 'gpt-4o-mini',
|
|
|
|
ai_model: 'gpt-4o-mini',
|
|
|
|
ai_model_writing: 'gpt-4o-mini',
|
|
|
|
ai_model_writing: 'gpt-4o-mini',
|
|
|
|
|
|
|
|
use_llm_for_source_links: false,
|
|
|
|
|
|
|
|
use_llm_for_article_extraction: false,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
expect(settingsResp.status).toBe(200);
|
|
|
|
expect(settingsResp.status).toBe(200);
|
|
|
|
|
|
|
|
|
|
|
|
@ -222,5 +225,47 @@ test.describe('Live generation with OpenAI', () => {
|
|
|
|
expect(item.summary.trim().length).toBeGreaterThan(50);
|
|
|
|
expect(item.summary.trim().length).toBeGreaterThan(50);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
// Comprehensive synthesis validation
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
const allUrls: string[] = [];
|
|
|
|
|
|
|
|
const domainCounts: Record<string, number> = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const section of synthesis.sections) {
|
|
|
|
|
|
|
|
for (const item of section.items) {
|
|
|
|
|
|
|
|
allUrls.push(item.url);
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const domain = new URL(item.url).hostname;
|
|
|
|
|
|
|
|
domainCounts[domain] = (domainCounts[domain] || 0) + 1;
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
// URL parse failure — already caught by earlier assertions
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Category article count check (including Autre)
|
|
|
|
|
|
|
|
expect(section.items.length).toBeLessThanOrEqual(4); // max_items_per_category
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// No duplicate URLs across all sections
|
|
|
|
|
|
|
|
const uniqueUrls = new Set(allUrls);
|
|
|
|
|
|
|
|
expect(uniqueUrls.size).toBe(allUrls.length);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// No domain exceeds max_articles_per_source (3)
|
|
|
|
|
|
|
|
for (const [domain, count] of Object.entries(domainCounts)) {
|
|
|
|
|
|
|
|
expect(count).toBeLessThanOrEqual(3);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Verify a sample of article links actually work (Playwright request API, no CORS issues)
|
|
|
|
|
|
|
|
const sampleUrls = allUrls.slice(0, 3);
|
|
|
|
|
|
|
|
for (const articleUrl of sampleUrls) {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const resp = await request.head(articleUrl);
|
|
|
|
|
|
|
|
expect(resp.status()).toBeGreaterThanOrEqual(200);
|
|
|
|
|
|
|
|
expect(resp.status()).toBeLessThan(400);
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
// Some sites block HEAD requests — skip rather than fail
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|