Initial commit: Converge portfolio rationalization app
Next.js app for mapping a customer's competitive tool landscape onto our portfolio and building the consolidation business case: assessment workbench, customer report and Word export, product catalog, competitive knowledge base, dossier import, shareable landscape questionnaires with a review inbox, tool-footprint and business-unit value analysis, and an implementation timeline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
79
scripts/test-report-leak.mjs
Normal file
79
scripts/test-report-leak.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* The account dossier holds internal pricing strategy, competitive risk and
|
||||
* personal data. None of it may reach a customer deliverable.
|
||||
*
|
||||
* Self-contained: builds its own assessment, imports the sample dossier through
|
||||
* the real endpoint, asserts on the redacted projection and the generated Word
|
||||
* file, then removes itself.
|
||||
*
|
||||
* (The rendered report page is verified in a browser; its server-rendered shell
|
||||
* carries no data, so asserting on that HTML would pass vacuously.)
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const B = 'http://127.0.0.1:3500';
|
||||
const json = (u, o) => fetch(B + u, o).then(r => r.json());
|
||||
const send = (u, m, b) => json(u, { method: m, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) });
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const t = (n, ok, x = '') => { ok ? (pass++, console.log(' PASS', n)) : (fail++, console.log(' FAIL', n, x)); };
|
||||
|
||||
const text = readFileSync(new URL('./fixtures/dossier-sample.txt', import.meta.url), 'utf8');
|
||||
const SOURCE = 'https://docs.google.com/document/d/LEAKTEST/edit';
|
||||
|
||||
const a = await send('/api/assessments', 'POST', { customerName: 'Redaction Test Co' });
|
||||
try {
|
||||
const imported = await send('/api/assessments/x/intel', 'POST', { text, sourceUrl: SOURCE });
|
||||
t('dossier parsed for the test', imported.proposals.length > 0 && !!imported.intel.company);
|
||||
|
||||
a.accountIntel = imported.intel;
|
||||
a.candidates = imported.proposals.slice(0, 3).map((p, i) => ({
|
||||
id: `c-${i}`, incumbentVendor: p.vendor, incumbentProduct: p.product, domain: p.domain,
|
||||
productIds: p.productIds, annualSpend: 500000, units: 0, unitLabel: '', renewalDate: '',
|
||||
capability: p.capability, switchEase: p.switchEase, timeToRoiMonths: p.timeToRoiMonths,
|
||||
savingsPct: p.savingsPct, migrationCost: 50000, strategicValue: 3,
|
||||
risks: '', notes: `From account dossier — ${p.evidence}`, included: true,
|
||||
}));
|
||||
await send(`/api/assessments/${a.id}`, 'PUT', a);
|
||||
|
||||
const full = await json(`/api/assessments/${a.id}`);
|
||||
t('stored record keeps the dossier', !!full.accountIntel);
|
||||
t('stored record keeps internal notes', full.candidates.every(c => c.notes));
|
||||
|
||||
const safe = await json(`/api/assessments/${a.id}/report`);
|
||||
t('projection drops the dossier', safe.accountIntel === null);
|
||||
t('projection drops internal notes', safe.candidates.every(c => c.notes === ''));
|
||||
t('projection keeps every candidate', safe.candidates.length === full.candidates.length);
|
||||
t('projection keeps the mappings', safe.candidates.every((c, i) => c.productIds.join() === full.candidates[i].productIds.join()));
|
||||
t('projection keeps the spend figures', safe.candidates.every((c, i) => c.annualSpend === full.candidates[i].annualSpend));
|
||||
|
||||
// A .docx is a ZIP: the bytes must be inflated or a text search passes vacuously.
|
||||
const buf = Buffer.from(await (await fetch(`${B}/api/assessments/${a.id}/docx`)).arrayBuffer());
|
||||
const tmp = new URL('./.leak-test.docx', import.meta.url);
|
||||
writeFileSync(tmp, buf);
|
||||
const docx = execFileSync('unzip', ['-p', tmp.pathname, 'word/document.xml'], { maxBuffer: 32e6 }).toString('utf8');
|
||||
t('Word file is a readable docx', docx.includes('<w:document') && docx.length > 2000);
|
||||
|
||||
const safeText = JSON.stringify(safe);
|
||||
const secrets = [
|
||||
SOURCE, 'docs.google.com', 'Tier 1 / Strategic', 'Renewal Risk', 'Intelligence Score',
|
||||
'TechDirector', 'Shelfware', 'Tech Debt Signals', 'AM-SOUTHEAST', 'From account dossier',
|
||||
'Everpure', 'Workload Distribution',
|
||||
];
|
||||
for (const s of secrets) {
|
||||
const label = s.length > 30 ? s.slice(0, 30) + '…' : s;
|
||||
t(`projection omits "${label}"`, !safeText.includes(s));
|
||||
t(`Word omits "${label}"`, !docx.includes(s));
|
||||
}
|
||||
|
||||
t('Word still names the customer', docx.includes('Redaction Test Co'));
|
||||
t('Word still carries a mapped product', /Avi Load Balancer|VCF Networking|Layer7/.test(docx));
|
||||
|
||||
execFileSync('rm', ['-f', tmp.pathname]);
|
||||
} finally {
|
||||
await fetch(`${B}/api/assessments/${a.id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
console.log(`\n${pass} passed, ${fail} failed`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
Reference in New Issue
Block a user