// Parser tests run against the compiled lib so they exercise the real source. const { parseDossier, buildImportResult } = await import('../.test-build/dossier.js'); import { readFileSync } from 'node:fs'; const raw = readFileSync(new URL('./fixtures/dossier-sample.txt', import.meta.url), 'utf8'); const kb = JSON.parse(readFileSync(new URL('../data/knowledge-base.json', import.meta.url), 'utf8')); const catalog = JSON.parse(readFileSync(new URL('../data/catalog.json', import.meta.url), 'utf8')); let pass=0, fail=0; const t=(n,ok,extra='')=>{ok?(pass++,console.log(' PASS',n)):(fail++,console.log(' FAIL',n,extra))}; const intel = parseDossier(raw, 'https://docs.google.com/document/d/ABC/edit'); t('company', intel.company === 'GUIDEWELL BLUE CROSS BLUE SHIELD OF FLORIDA', intel.company); t('industry', intel.industry === 'Health Care Equipment & Services', intel.industry); t('classification', intel.classification === 'Tier 1 / Strategic'); t('renewal timeframe', intel.renewalTimeframe === '2028 Q 2'); t('"Not available" is dropped, not stored', !JSON.stringify(intel).includes('Not available')); t('source url retained', intel.sourceUrl.includes('docs.google.com')); t('infrastructure vendors', intel.infrastructure.length === 5, JSON.stringify(intel.infrastructure)); t('load balancer is F5', intel.infrastructure.some(f=>f.label==='Load Balancer Vendor' && f.value==='F5')); t('cloud facts', intel.cloud.length === 3); t('vcf facts captured', intel.vcf.some(f=>f.label==='VCF Cores Entitled' && f.value==='35768')); t('consumption rows', intel.consumption.length === 7, String(intel.consumption.length)); const vsphere = intel.consumption.find(c=>c.product==='VSPHERE'); t('vsphere entitled/deployed parsed', vsphere.entitled===35768 && vsphere.deployed===33400 && vsphere.adoption==='High'); t('deployment rate', intel.deploymentRate.startsWith('76%')); t('apps parsed across categories', intel.applications.length === 19, String(intel.applications.length)); t('app category assigned', intel.applications.find(a=>a.name==='SAP Finance')?.category === 'ERP'); t('bullet chars stripped', intel.applications.every(a=>!/^[•\-\s]/.test(a.name))); t('narratives captured', intel.narratives.length === 3, JSON.stringify(intel.narratives.map(n=>n.heading))); t('narrative body', /Mainframe at 15%/.test(intel.narratives.find(n=>n.heading==='Tech Debt Signals').body)); // ---- proposals ---- const res = buildImportResult(raw, '', kb, catalog); const by = (l) => res.proposals.find(p=>p.label.toLowerCase().includes(l)); t('F5 proposed', !!by('f5'), res.proposals.map(p=>p.label).join(' | ')); t('F5 maps to Avi', by('f5')?.productIds.includes('ans-avi')); t('Zscaler proposed', !!by('zscaler')); t('IBM API Connect proposed', !!by('api connect') || !!by('ibm api')); t('no duplicate KB entries', new Set(res.proposals.map(p=>p.kbEntryId)).size === res.proposals.length); t('all mapped product ids are real', res.proposals.every(p=>p.productIds.every(id=>catalog.products.some(x=>x.id===id)))); t('proposals carry evidence', res.proposals.every(p=>p.evidence.includes(':'))); t('sorted by confidence', res.proposals.map(p=>({high:0,medium:1,low:2})[p.confidence]).every((v,i,a)=>i===0||a[i-1]<=v)); t('shelfware = entitled>0 & deployed=0', res.shelfware.length===2 && res.shelfware.every(c=>c.entitled>0&&c.deployed===0), JSON.stringify(res.shelfware.map(c=>c.product))); t('vsphere not shelfware', !res.shelfware.some(c=>c.product==='VSPHERE')); // Garbage in must not throw. const empty = buildImportResult('hello world', '', kb, catalog); t('junk input degrades gracefully', empty.proposals.length===0 && empty.warnings.length>0); console.log(`\n${pass} passed, ${fail} failed`); console.log(`\nProposals from the sample dossier (${res.proposals.length}):`); for (const p of res.proposals) { const names = p.productIds.map(id=>catalog.products.find(x=>x.id===id)?.name).join(', '); console.log(` [${p.confidence.padEnd(6)}] ${p.label.padEnd(26)} → ${names || '(unmapped)'}`); } process.exit(fail?1:0);